parent
035aa4922a
commit
a933aa4541
@ -0,0 +1,83 @@ |
||||
""" Manage loading and saving files. """ |
||||
|
||||
import os |
||||
|
||||
from PyQt5.QtWidgets import QFileDialog |
||||
|
||||
# --------------------------------------------------------------------- |
||||
|
||||
# NOTE: While loading/saving files works fine when handled by the embedded browser, |
||||
# we can't get the full path of the file loaded (because of browser security). |
||||
# This means that we can't do things like default to saving a scenario to the same file |
||||
# it was loaded from, or retrying a failed save. This is such a lousy UX, |
||||
# we handle load/save operations ourself, where we can manage things like this. |
||||
|
||||
class FileDialog: |
||||
"""Manage loading and saving files.""" |
||||
|
||||
def __init__( self, parent, object_name, default_extn, filters, default_fname ): |
||||
self.parent = parent |
||||
self.object_name = object_name |
||||
self.default_extn = default_extn |
||||
self.filters = filters |
||||
self.curr_fname = default_fname |
||||
|
||||
def load_file( self, binary ): |
||||
"""Load a file.""" |
||||
|
||||
# ask the user which file to load |
||||
fname, _ = QFileDialog.getOpenFileName( |
||||
self.parent, "Load {}".format( self.object_name ), |
||||
self.curr_fname, |
||||
self.filters |
||||
) |
||||
if not fname: |
||||
return None |
||||
|
||||
# load the file |
||||
try: |
||||
with open( fname, "rb" ) as fp: |
||||
data = fp.read() |
||||
except Exception as ex: #pylint: disable=broad-except |
||||
self.parent.showErrorMsg( "Can't load the {}:\n\n{}".format( self.object_name, ex ) ) |
||||
return None |
||||
if not binary: |
||||
data = data.decode( "utf-8" ) |
||||
self.curr_fname = fname |
||||
|
||||
return data |
||||
|
||||
def save_file( self, data ): |
||||
"""Save data to a file.""" |
||||
|
||||
# initialize |
||||
if isinstance( data, str ): |
||||
data = data.encode( "utf-8" ) |
||||
|
||||
while True: # nb: keep trying until the save succeeds or the user cancels the operation |
||||
|
||||
# ask the user where to save the file |
||||
fname, _ = QFileDialog.getSaveFileName( |
||||
self.parent, "Save {}".format( self.object_name), |
||||
self.curr_fname, |
||||
self.filters |
||||
) |
||||
if not fname: |
||||
return False |
||||
|
||||
# check the file extension |
||||
extn = os.path.splitext( fname )[1] |
||||
if not extn: |
||||
fname += self.default_extn |
||||
elif fname.endswith( "." ): |
||||
fname = fname[:-1] |
||||
|
||||
# save the file |
||||
try: |
||||
with open( fname, "wb", ) as fp: |
||||
fp.write( data ) |
||||
except Exception as ex: #pylint: disable=broad-except |
||||
self.parent.showErrorMsg( "Can't save the {}:\n\n{}".format( self.object_name, ex ) ) |
||||
continue |
||||
self.curr_fname = fname |
||||
return True |
@ -1,4 +1,10 @@ |
||||
[Site Config] |
||||
|
||||
; Enable VASL counter images in the UI by configuring a VASL .vmod file here. |
||||
VASL_MOD = ... |
||||
VASL_MOD = ...configure the VASL module (e.g. vasl-6.4.3.vmod)... |
||||
|
||||
; Configure VASSAL to be able to automatically update labels in a VASL scenario. |
||||
VASSAL_DIR = ...configure the VASSAL installation directory... |
||||
BOARDS_DIR = ...configure the VASL boards directory... |
||||
WEBDRIVER_PATH = ...configure either geckodriver or chromedriver here... |
||||
; JAVA_PATH = ...configure the Java executable here (optional, must be in the PATH otherwise)... |
||||
|
@ -0,0 +1,9 @@ |
||||
.ui-dialog.update-vsav .ui-dialog-titlebar { display: none ; } |
||||
#update-vsav { display: flex ; align-items: center ; } |
||||
#update-vsav img { margin-right: 1em ; } |
||||
|
||||
#vassal-shim-error textarea { width: 100% ; height: 15em ; min-height: 5em ; resize: none ; padding: 2px ; font-family: monospace ; font-size: 80% ; } |
||||
.ui-dialog.vassal-shim-error .ui-dialog-titlebar { background: #f5af41 ; } |
||||
.ui-dialog.vassal-shim-error .ui-dialog-content { display: flex ; flex-direction: column ; } |
||||
.ui-dialog.vassal-shim-error .ui-dialog-content textarea { flex-grow: 1 ; } |
||||
.ui-dialog.vassal-shim-error .ui-dialog-buttonpane { border: none ; margin-top: 0 !important ; padding-top: 0 !important ; } |
@ -0,0 +1,291 @@ |
||||
|
||||
// --------------------------------------------------------------------
|
||||
|
||||
function on_update_vsav() |
||||
{ |
||||
// FOR TESTING PORPOISES! We can't control a file upload from Selenium (since
|
||||
// the browser will use native controls), so we get the data from a <textarea>).
|
||||
if ( getUrlParam( "vsav_persistence" ) ) { |
||||
var $elem = $( "#_vsav-persistence_" ) ; |
||||
var vsav_data = $elem.val() ; |
||||
$elem.val( "" ) ; // nb: let the test suite know we've received the data
|
||||
do_update_vsav( vsav_data, "test.vsav" ) ; |
||||
return ; |
||||
} |
||||
|
||||
// if we are running inside the PyQt wrapper, let it handle everything
|
||||
if ( gWebChannelHandler ) { |
||||
gWebChannelHandler.load_vsav( function( data ) { |
||||
if ( ! data ) |
||||
return ; |
||||
do_update_vsav( data.data, data.filename ) ; |
||||
} ) ; |
||||
return ; |
||||
} |
||||
|
||||
// ask the user to upload the VSAV file
|
||||
$("#load-vsav").trigger( "click" ) ; // nb: this will call on_load_vsav_file_selected() when a file has been selected
|
||||
} |
||||
|
||||
function on_load_vsav_file_selected() |
||||
{ |
||||
// read the selected file
|
||||
var fileReader = new FileReader() ; |
||||
var file = $("#load-vsav").prop( "files" )[0] ; |
||||
fileReader.onload = function() { |
||||
vsav_data = fileReader.result ; |
||||
if ( vsav_data.substring(0,5) === "data:" ) |
||||
vsav_data = vsav_data.split( "," )[1] ; |
||||
do_update_vsav( vsav_data, file.name ) ; |
||||
} ; |
||||
fileReader.readAsDataURL( file ) ; |
||||
} |
||||
|
||||
function do_update_vsav( vsav_data, fname ) |
||||
{ |
||||
// show the progress dialog
|
||||
var $dlg = $( "#update-vsav" ).dialog( { |
||||
dialogClass: "update-vsav", |
||||
modal: true, |
||||
width: 300, |
||||
height: 60, |
||||
resizable: false, |
||||
closeOnEscape: false, |
||||
} ) ; |
||||
|
||||
// generate all the snippets
|
||||
var snippets = _generate_snippets() ; |
||||
|
||||
// send a request to update the VSAV
|
||||
var data = { "filename": fname, vsav_data: vsav_data, snippets: snippets } ; |
||||
$.ajax( { |
||||
url: gUpdateVsavUrl, |
||||
type: "POST", |
||||
data: JSON.stringify( data ), |
||||
contentType: "application/json", |
||||
} ).done( function( data ) { |
||||
$dlg.dialog( "close" ) ; |
||||
data = JSON.parse( data ) ; |
||||
// check if there was an error
|
||||
if ( data.error ) { |
||||
if ( getUrlParam( "vsav_persistence" ) ) { |
||||
$("#_vsav-persistence_").val( |
||||
"ERROR: " + data.error + "\n\n=== STDOUT ===\n" + data.stdout + "\n=== STDERR ===\n" + data.stderr |
||||
) ; |
||||
return ; |
||||
} |
||||
$("#vassal-shim-error").dialog( { |
||||
dialogClass: "vassal-shim-error", |
||||
title: "Scenario update error", |
||||
modal: true, |
||||
width: 600, height: "auto", |
||||
open: function() { |
||||
$( "#vassal-shim-error .message" ).html( data.error ) ; |
||||
var log = "" ; |
||||
if ( data.stdout && data.stderr ) |
||||
log = "=== STDOUT ===" + data.stdout + "\n=== STDERR ===\n" + data.stderr ; |
||||
else if ( data.stdout ) |
||||
log = data.stdout ; |
||||
else if ( data.stderr ) |
||||
log = data.stderr ; |
||||
if ( log ) |
||||
$( "#vassal-shim-error .log" ).val( log ).show() ; |
||||
else |
||||
$( "#vassal-shim-error .log" ).hide() ; |
||||
}, |
||||
buttons: { |
||||
Close: function() { $(this).dialog( "close" ) ; }, |
||||
}, |
||||
} ) ; |
||||
return ; |
||||
} |
||||
// check if anything was changed
|
||||
if ( ! data.report.was_modified ) { |
||||
showInfoMsg( "No changes were made to the VASL scenario." ) ; |
||||
if ( getUrlParam( "vsav_persistence" ) ) |
||||
$("#_vsav-persistence_").val( btoa( "No changes." ) ) ; |
||||
return ; |
||||
} |
||||
// save the updated VSAV file
|
||||
if ( gWebChannelHandler ) { |
||||
gWebChannelHandler.save_updated_vsav( data.filename, data.vsav_data, function( resp ) { |
||||
if ( resp ) |
||||
_show_label_report_msg( data.report ) ; |
||||
} ) ; |
||||
return ; |
||||
} |
||||
_show_label_report_msg( data.report ) ; |
||||
if ( getUrlParam( "vsav_persistence" ) ) { |
||||
// FOR TESTING PORPOISES! We can't control a file download from Selenium (since
|
||||
// the browser will use native controls), so we store the result in a <textarea>
|
||||
// and the test suite will collect it from there).
|
||||
$("#_vsav-persistence_").val( data.vsav_data ) ; |
||||
return ; |
||||
} |
||||
download( atob(data.vsav_data), data.filename, "application/octet-stream" ) ; |
||||
} ).fail( function( xhr, status, errorMsg ) { |
||||
$dlg.dialog( "close" ) ; |
||||
showErrorMsg( "Can't update the VASL scenario:<div class='pre'>" + escapeHTML(errorMsg) + "</div>" ) ; |
||||
} ) ; |
||||
} |
||||
|
||||
function _show_label_report_msg( report ) |
||||
{ |
||||
// generate a message summarizing what the VASSAL shim did
|
||||
var buf = [ "The VASL scenario was updated:", "<ul>" ] ; |
||||
var actions = [ "created", "updated", "deleted" ] ; // nb: we ignore "unchanged"
|
||||
for ( var i=0 ; i < actions.length ; ++i ) { |
||||
var action = actions[i] ; |
||||
var n = parseInt( report[ "labels_"+action ] ) ; |
||||
if ( n == 1 ) |
||||
buf.push( "<li>1 label was " + action + "." ) ; |
||||
else if ( n > 1 ) |
||||
buf.push( "<li>" + n + " labels were " + action + "." ) ; |
||||
} |
||||
buf.push( "</ul>" ) ; |
||||
var msg = buf.join( "" ) ; |
||||
|
||||
// show the message
|
||||
if ( report.labels_deleted > 0 ) |
||||
showWarningMsg( msg ) ; |
||||
else |
||||
showInfoMsg( msg ) ; |
||||
} |
||||
|
||||
// --------------------------------------------------------------------
|
||||
|
||||
function _generate_snippets() |
||||
{ |
||||
// initialize
|
||||
var snippets = {} ; |
||||
|
||||
// figure out which templates we don't want to auto-create labels for
|
||||
var no_autocreate = {} ; |
||||
for ( var nat in NATIONALITY_SPECIFIC_BUTTONS ) { |
||||
for ( var i=0 ; i < NATIONALITY_SPECIFIC_BUTTONS[nat].length ; ++i ) { |
||||
var template_id = NATIONALITY_SPECIFIC_BUTTONS[nat][i] ; |
||||
if ( ["pf","atmm"].indexOf( template_id ) !== -1 ) { |
||||
// NOTE: PF and ATMM are always available as an inherent part of a squad's capabilities (subject to date restrictions),
|
||||
// so we always auto-create these labels, unlike, say MOL or BAZ, which are only present by SSR or OB counter).
|
||||
continue ; |
||||
} |
||||
no_autocreate[template_id] = true ; |
||||
} |
||||
} |
||||
|
||||
function on_snippet_button( $btn, inactive ) { |
||||
var template_id = $btn.attr( "data-id" ) ; |
||||
if ( template_id.substr(0,7) === "extras/" ) { |
||||
// NOTE: We don't handle extras templates, since they can be parameterized. We would need to store
|
||||
// the parameter values in the generated snippet, and extract them here so that we can re-generate
|
||||
// the snippet, which is more trouble than it's worth, at this point.
|
||||
return ; |
||||
} |
||||
var snippet_id = template_id ; |
||||
var extra_params = {} ; |
||||
var player_no = get_player_no_for_element( $btn ) ; |
||||
if ( ["scenario_note","ob_setup","ob_note"].indexOf( template_id ) !== -1 ) { |
||||
var data = $btn.parent().parent().data( "sortable2-data" ) ; |
||||
if ( player_no ) |
||||
snippet_id = template_id + "_" + player_no + "." + data.id ; |
||||
else |
||||
snippet_id = template_id + "." + data.id ; |
||||
extra_params = get_simple_note_snippet_extra_params( $btn ) ; |
||||
} |
||||
var raw_content = _get_raw_content( snippet_id, $btn ) ; |
||||
if ( ["scenario","players","victory_conditions"].indexOf( snippet_id ) === -1 ) { |
||||
// NOTE: We don't pass through a snippet for things that have no content,
|
||||
// except for important stuff, such as the scenario name and victory conditions.
|
||||
if ( raw_content === null || raw_content.length === 0 ) { |
||||
return ; |
||||
} |
||||
} |
||||
snippets[snippet_id] = { |
||||
content: make_snippet( $btn, extra_params, false ), |
||||
auto_create: ! no_autocreate[template_id] && ! inactive, |
||||
raw_content: raw_content, |
||||
} ; |
||||
if ( player_no ) |
||||
snippets[snippet_id].label_area = "player" + player_no ; |
||||
} |
||||
$("button.generate").each( function() { |
||||
if ( $(this).parent().css( "display" ) === "none" ) |
||||
return ; |
||||
on_snippet_button( $(this), $(this).hasClass("inactive") ) ; |
||||
} ) ; |
||||
$("img.snippet").each( function() { |
||||
on_snippet_button( $(this) ) ; |
||||
} ) ; |
||||
|
||||
return snippets ; |
||||
} |
||||
|
||||
function _get_raw_content( snippet_id, $btn ) |
||||
{ |
||||
// NOTE: We pass the raw content, as entered by the user into the UI, through to the VASSAL shim,
|
||||
// so that it can locate legacy labels, that were created before we added snippet ID's to the templates.
|
||||
|
||||
var raw_content = [] ; |
||||
function get_values( names ) { |
||||
for ( var i=0 ; i < names.length ; ++i ) { |
||||
var val = $( ".param[name='" + names[i] + "']" ).val().trim() ; |
||||
if ( val ) |
||||
raw_content.push( val ) ; |
||||
} |
||||
return raw_content ; |
||||
} |
||||
|
||||
// handle special cases
|
||||
if ( snippet_id === "scenario" ) |
||||
return get_values([ "SCENARIO_NAME", "SCENARIO_ID", "SCENARIO_LOCATION" ]) ; |
||||
if ( snippet_id === "victory_conditions" ) |
||||
return get_values([ "VICTORY_CONDITIONS" ]) ; |
||||
if ( snippet_id === "players" ) { |
||||
return [ |
||||
"ELR:", "SAN:", |
||||
get_nationality_display_name( get_player_nat( 1 ) ) + ":", |
||||
get_nationality_display_name( get_player_nat( 2 ) ) + ":", |
||||
] ; |
||||
} |
||||
if ( snippet_id === "ssr" ) { |
||||
$( "#ssr-sortable > li" ).each( function() { |
||||
var data = $(this).data( "sortable2-data" ) ; |
||||
raw_content.push( data.caption ) ; |
||||
} ) ; |
||||
return raw_content ; |
||||
} |
||||
|
||||
// handle simple cases
|
||||
if ( snippet_id === "mol" ) |
||||
return [ "Molotov Cocktail", "MOL check:", "IFT DR original colored dr:" ] ; |
||||
if ( snippet_id === "mol-p" ) |
||||
return [ "MOL Projector", "TH#", "X#", "B#" ] ; |
||||
if ( snippet_id === "pf" ) |
||||
return [ "Panzerfaust", "PF check:", "non-AFV target", "TH#" ] ; |
||||
if ( snippet_id === "psk" ) |
||||
return [ "Panzerschrek", "Range", "TH#", "X#", "TK#" ] ; |
||||
if ( snippet_id === "atmm" ) |
||||
return [ "Anti-Tank Magnetic Mines", "ATMM check:", "vs. non-armored vehicle" ] ; |
||||
if ( snippet_id === "piat" ) |
||||
return [ "PIAT", "Range", "TH#", "B#", "TK#" ] ; |
||||
if ( snippet_id === "baz" ) |
||||
return [ "Bazooka", "Range", "TH#" ] ; |
||||
|
||||
// handle simple notes
|
||||
if ( $btn.prop( "tagName" ).toLowerCase() == "img" ) { |
||||
var data = $btn.parent().parent().data( "sortable2-data" ) ; |
||||
return [ data.caption ] ; |
||||
} |
||||
|
||||
// handle vehicles/ordnance
|
||||
if ( snippet_id.substring(0,11) === "ob_vehicles" || snippet_id.substring(0,11) === "ob_ordnance" ) { |
||||
var id = snippet_id.substring(0,11) + "-sortable" + snippet_id.substring(11) ; |
||||
$( "#"+id + " > li" ).each( function() { |
||||
var vo_entry = $(this).data( "sortable2-data" ).vo_entry ; |
||||
raw_content.push( vo_entry.name ) ; |
||||
} ) ; |
||||
return raw_content ; |
||||
} |
||||
|
||||
return null ; |
||||
} |
@ -0,0 +1,9 @@ |
||||
<div id="update-vsav" style="display:none;"> |
||||
<img src="{{url_for('static',filename='images/loader.gif')}}"> |
||||
Updating your VASL scenario... |
||||
</div> |
||||
|
||||
<div id="vassal-shim-error" style="display:none;"> |
||||
<div class="message"></div> |
||||
<textarea class="log"></textarea> |
||||
</div> |
@ -0,0 +1,57 @@ |
||||
SetupCommand: starting=false |
||||
AddPiece: DynamicProperty/81* MTR GrW 34 ca3 |
||||
AddPiece: DynamicProperty/3-3-8 Ehs |
||||
AddPiece: DynamicProperty/PzKw VG vca3 tca3 BU |
||||
AddPiece: DynamicProperty/HMG |
||||
AddPiece: Stack |
||||
AddPiece: DynamicProperty/User-Labeled |
||||
- test<09>global |
||||
- null |
||||
- <empty> |
||||
- Label |
||||
- Line 2 |
||||
- Map0;84;76;6292 |
||||
AddPiece: Stack |
||||
AddPiece: DynamicProperty/User-Labeled |
||||
- test<09>global |
||||
- null |
||||
- <empty> |
||||
- Label |
||||
- no background |
||||
- Map0;231;79;6293 |
||||
AddPiece: Stack |
||||
AddPiece: DynamicProperty/User-Labeled |
||||
- test<09>global |
||||
- null |
||||
- <empty> |
||||
- Label |
||||
- Line 2 |
||||
- Map0;366;79;6294 |
||||
AddPiece: Stack |
||||
AddPiece: DynamicProperty/User-Labeled |
||||
- test<09>global |
||||
- null |
||||
- <empty> |
||||
- Label |
||||
- no background |
||||
- Map0;498;82;6295 |
||||
AddPiece: DynamicProperty/User-Labeled |
||||
- test<09>global |
||||
- null |
||||
- <empty> |
||||
- Label |
||||
- Line 2 |
||||
- Map0;648;85;6296 |
||||
AddPiece: DynamicProperty/User-Labeled |
||||
- test<09>global |
||||
- null |
||||
- <empty> |
||||
- Label |
||||
- no background |
||||
- Map0;777;90;6297 |
||||
SetInfo |
||||
SetAllowed: [] |
||||
SetBoards |
||||
SetScenarioNote |
||||
SetPublicNote |
||||
SetupCommand: starting=true |
Binary file not shown.
Binary file not shown.
@ -0,0 +1 @@ |
||||
{"SCENARIO_NAME":"test scenario","SCENARIO_ID":"TEST-01","SCENARIO_LOCATION":"Somewhere","SCENARIO_DATE":"2001-02-03","SCENARIO_WIDTH":"123","VICTORY_CONDITIONS_WIDTH":"300px","SSR_WIDTH":"300px","OB_VEHICLES_WIDTH_1":"","OB_ORDNANCE_WIDTH_1":"","OB_VEHICLES_WIDTH_2":"","OB_ORDNANCE_WIDTH_2":"","VICTORY_CONDITIONS":"Make the other guy die for his country!","SCENARIO_THEATER":"Burma","PLAYER_1":"american","PLAYER_1_ELR":"1","PLAYER_1_SAN":"2","PLAYER_2":"belgian","PLAYER_2_ELR":"3","PLAYER_2_SAN":"4","SSR":["SSR #1","SSR #2","SSR #3"],"OB_VEHICLES_1":[{"id":"am/v:008","name":"M4A1"},{"id":"am/v:021","name":"Sherman Crab"}],"OB_VEHICLES_2":[{"id":"alc/v:006","name":"R-35(f)"},{"id":"alc/v:012","name":"Medium Truck"}],"OB_ORDNANCE_1":[{"id":"am/o:002","name":"M1 81mm Mortar"}],"OB_ORDNANCE_2":[{"id":"alc/o:006","name":"Bofors M34"},{"id":"be/o:000","name":"DBT"}],"SCENARIO_NOTES":[{"caption":"scenario note #1","width":"200px","id":1}],"OB_SETUPS_1":[{"caption":"U.S. setup #1","id":1,"width":"111"},{"caption":"U.S. setup #2","id":2,"width":""},{"caption":"U.S. setup #3","id":3,"width":""}],"OB_SETUPS_2":[{"caption":"Belgian setup #1","id":1,"width":""},{"caption":"Belgian setup #2","id":2,"width":"222"},{"caption":"Belgian setup #3","id":3,"width":""}],"OB_NOTES_1":[{"caption":"U.S. note #1","id":1,"width":"111"},{"caption":"U.S. note #2","id":2,"width":""}],"OB_NOTES_2":[{"caption":"Belgian note #1","id":1,"width":""},{"caption":"Belgian note #2","id":2,"width":"222"}]} |
Binary file not shown.
@ -0,0 +1 @@ |
||||
{"SCENARIO_NAME":"Hill 621","SCENARIO_ID":"ASL E","SCENARIO_LOCATION":"Near Minsk, Russia","SCENARIO_DATE":"1944-06-29","SCENARIO_WIDTH":"","VICTORY_CONDITIONS_WIDTH":"240px","SSR_WIDTH":"300px","OB_VEHICLES_WIDTH_1":"","OB_ORDNANCE_WIDTH_1":"","OB_VEHICLES_WIDTH_2":"","OB_ORDNANCE_WIDTH_2":"","VICTORY_CONDITIONS":"The Russians win at Game End if they Control ≥ five Level 3 hill hexes on Board 2.","PLAYER_1":"russian","PLAYER_1_ELR":"4","PLAYER_1_SAN":"3","PLAYER_2":"german","PLAYER_2_ELR":"3","PLAYER_2_SAN":"4","SSR":["EC are Moderate, with no wind at start.","After \"At Start\" placement, each German infantry unit must take a TC. The only possible consequence of failure is that the unit must begin the scenario broken. Those units which break during this pre-game TC are not subject to DM in the initial German RPh.","The Germans receive one module of 80+mm Battalion Mortar OBA (HE and Smoke) with the radio in the initial OB.","The Germans receive one module of 100+mm OBA (HE and Smoke) with the Turn 4 reinforcements."],"OB_VEHICLES_1":[{"name":"T-34 M43"},{"name":"SU-152"},{"name":"SU-122"},{"name":"ZIS-5"}],"OB_VEHICLES_2":[{"name":"PzKpfw IVH"},{"name":"PzKpfw IIIN"},{"name":"StuG IIIG (L)"},{"name":"StuH 42"},{"name":"SPW 250/1"},{"name":"SPW 251/1"},{"name":"SPW 251/sMG"}],"OB_ORDNANCE_2":[{"name":"7.5cm PaK 40"},{"name":"5cm PaK 38"}],"SCENARIO_NOTES":[{"caption":"Download the scenario card from <a href=\"http://www.multimanpublishing.com/Support/ASLASLSK/ASLOfficialDownloads/tabid/109/Default.aspx\">Multi-Man Publishing</a> (ASL Classic pack).","width":"300px"}],"OB_SETUPS_1":[{"caption":"Set up on any whole hex of Board 3","width":""},{"caption":"Enter on Turn 2 on any single road hex <br>\non the east edge of Board 3","width":""},{"caption":"Enter on Turn 5 on any single road hex <br>\non the east edge of Board 3","width":""}],"OB_SETUPS_2":[{"caption":"Set up in any whole hex of Board 4","width":""},{"caption":"Enter on Turn 1 on any single road hex <br>\non any edge of Board 2","width":""},{"caption":"Enter on Turn 2 on any single road hex <br>\non the north <i>or</i> south edge of Board 4","width":""},{"caption":"Enter on Turn 4 on any single road hex <br>\non the west edge of Board 2","width":""},{"caption":"Enter on Turn 5 on any single road hex <br>\nalong the north, south or west edge of Board 2","width":""},{"caption":"Enter on Turn 8 along <br>\nthe west edge of Board 2","width":""}],"OB_NOTES_1":[],"OB_NOTES_2":[{"caption":"80+mm Battalion Mortar <br> OBA (HE/Smoke)","width":""},{"caption":"100+mm OBA (HE/Smoke)","width":""}]} |
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -0,0 +1,574 @@ |
||||
""" Test VASSAL integration. """ |
||||
|
||||
import os |
||||
import glob |
||||
import re |
||||
import json |
||||
import base64 |
||||
import random |
||||
import typing.re #pylint: disable=import-error |
||||
|
||||
import pytest |
||||
|
||||
from vasl_templates.webapp.config.constants import DATA_DIR as REAL_DATA_DIR |
||||
from vasl_templates.webapp.vassal import VassalShim |
||||
from vasl_templates.webapp.utils import TempFile, change_extn |
||||
from vasl_templates.webapp.tests.utils import \ |
||||
init_webapp, select_menu_option, get_stored_msg, set_stored_msg, set_stored_msg_marker, wait_for |
||||
from vasl_templates.webapp.tests.test_scenario_persistence import load_scenario, load_scenario_params, \ |
||||
assert_scenario_params_complete |
||||
|
||||
# --------------------------------------------------------------------- |
||||
|
||||
@pytest.mark.skipif( not pytest.config.option.vasl_mods, reason="--vasl-mods not specified" ) #pylint: disable=no-member |
||||
@pytest.mark.skipif( not pytest.config.option.vassal, reason="--vassal not specified" ) #pylint: disable=no-member |
||||
@pytest.mark.skipif( pytest.config.option.short_tests, reason="--short-tests specified" ) #pylint: disable=no-member |
||||
def test_full_update( webapp, webdriver, monkeypatch ): |
||||
"""Test updating a scenario that contains the full set of snippets.""" |
||||
|
||||
# initialize |
||||
monkeypatch.setitem( webapp.config, "DATA_DIR", REAL_DATA_DIR ) |
||||
init_webapp( webapp, webdriver, vsav_persistence=1 ) |
||||
|
||||
# NOTE: We disable this for speed, since we don't care about label positioning. |
||||
monkeypatch.setitem( webapp.config, "DISABLE_UPDATE_VSAV_SCREENSHOTS", True ) |
||||
|
||||
# load the scenario fields |
||||
SCENARIO_PARAMS = { |
||||
"scenario": { |
||||
"SCENARIO_NAME": "Modified scenario name (<>{}\"'\\)", |
||||
"SCENARIO_ID": "xyz123", |
||||
"SCENARIO_LOCATION": "Right here", |
||||
"SCENARIO_THEATER": "PTO", |
||||
"SCENARIO_DATE": "12/31/1945", |
||||
"SCENARIO_WIDTH": "101", |
||||
"PLAYER_1": "russian", "PLAYER_1_ELR": "5", "PLAYER_1_SAN": "4", |
||||
"PLAYER_2": "german", "PLAYER_2_ELR": "3", "PLAYER_2_SAN": "2", |
||||
"VICTORY_CONDITIONS": "Just do it!", "VICTORY_CONDITIONS_WIDTH": "102", |
||||
"SCENARIO_NOTES": [ |
||||
{ "caption": "Modified scenario note #1", "width": "" }, |
||||
{ "caption": "Modified scenario note #2", "width": "100px" } |
||||
], |
||||
"SSR": [ "Modified SSR #1", "Modified SSR #2" ], |
||||
"SSR_WIDTH": "103", |
||||
}, |
||||
"ob1": { |
||||
"OB_SETUPS_1": [ |
||||
{ "caption": "Modified Russian setup #1", "width": "" }, |
||||
{ "caption": "Modified Russian setup #2", "width": "200px" }, |
||||
{ "caption": "Modified Russian setup #3", "width": "" }, |
||||
{ "caption": "Modified Russian setup #4", "width": "" }, |
||||
{ "caption": "Modified Russian setup #5", "width": "" }, |
||||
], |
||||
"OB_NOTES_1": [ |
||||
{ "caption": "Modified Russian note #1", "width": "10em" }, |
||||
], |
||||
"OB_VEHICLES_1": [ "T-34/85 (MT)" ], |
||||
"OB_VEHICLES_WIDTH_1": "202", |
||||
"OB_ORDNANCE_1": [ "82mm BM obr. 37 (MTR)" ], |
||||
"OB_ORDNANCE_WIDTH_1": "203", |
||||
}, |
||||
"ob2": { |
||||
"OB_SETUPS_2": [ { "caption": "Modified German setup #1", "width": "" } ], |
||||
"OB_NOTES_2": [ |
||||
{ "caption": "Modified German note #1", "width": "" }, |
||||
{ "caption": "Modified German note #2", "width": "" }, |
||||
{ "caption": "Modified German note #3", "width": "" }, |
||||
{ "caption": "Modified German note #4", "width": "" }, |
||||
{ "caption": "Modified German note #5", "width": "" }, |
||||
], |
||||
"OB_VEHICLES_2": [ "PzKpfw VG (MT)" ], |
||||
"OB_VEHICLES_WIDTH_2": "302", |
||||
"OB_ORDNANCE_2": [ "3.7cm PaK 35/36 (AT)" ], |
||||
"OB_ORDNANCE_WIDTH_2": "303", |
||||
}, |
||||
} |
||||
load_scenario_params( SCENARIO_PARAMS ) |
||||
assert_scenario_params_complete( SCENARIO_PARAMS ) |
||||
|
||||
def do_test(): #pylint: disable=missing-docstring |
||||
|
||||
# dump the original VASL scenario |
||||
# NOTE: We could arguably only do this once, but updating scenarios is the key functionality of the VASSAL shim, |
||||
# and so it's worth checking that every VASSAL+VASL combination understands its input correctly. |
||||
fname = os.path.join( os.path.split(__file__)[0], "fixtures/update-vsav/full.vsav" ) |
||||
vassal_shim = VassalShim() |
||||
vsav_dump = vassal_shim.dump_scenario( fname ) |
||||
_check_vsav_dump( vsav_dump, { |
||||
"scenario": "Somewhere", |
||||
"players": re.compile( r"American:.*Belgian:" ), |
||||
"victory_conditions": "Make the other guy", |
||||
"ssr": re.compile( r"SSR #1.*SSR #2.*SSR #3" ), |
||||
"scenario_note.1": "scenario note #1", |
||||
"ob_setup_1.1": "U.S. setup #1", "ob_setup_1.2": "U.S. setup #2", "ob_setup_1.3": "U.S. setup #3", |
||||
"ob_note_1.1": "U.S. note #1", "ob_note_1.2": "U.S. note #2", |
||||
"ob_vehicles_1": re.compile( r"M4A1.*Sherman Crab" ), |
||||
"ob_ordnance_1": "M1 81mm Mortar", |
||||
"baz": "Bazooka", |
||||
"ob_setup_2.1": "Belgian setup #1", "ob_setup_2.2": "Belgian setup #2", "ob_setup_2.3": "Belgian setup #3", |
||||
"ob_note_2.1": "Belgian note #1", "ob_note_2.2": "Belgian note #2", |
||||
"ob_vehicles_2": re.compile( r"R-35\(f\).*Medium Truck" ), |
||||
"ob_ordnance_2": re.compile( r"Bofors M34.*DBT" ), |
||||
} ) |
||||
|
||||
# update the VASL scenario with the new snippets |
||||
updated_vsav_data = _update_vsav( fname, { "created": 8, "updated": 16, "deleted": 4 } ) |
||||
with TempFile() as temp_file: |
||||
# check the results |
||||
temp_file.write( updated_vsav_data ) |
||||
temp_file.close() |
||||
updated_vsav_dump = vassal_shim.dump_scenario( temp_file.name ) |
||||
_check_vsav_dump( updated_vsav_dump, { |
||||
"scenario": "Modified scenario name (<>{}\"'\\)", |
||||
"players": re.compile( r"Russian:.*German:" ), |
||||
"victory_conditions": "Just do it!", |
||||
"ssr": re.compile( r"Modified SSR #1.*Modified SSR #2" ), |
||||
"scenario_note.1": "Modified scenario note #1", |
||||
"scenario_note.2": "Modified scenario note #2", |
||||
"ob_setup_1.1": "Modified Russian setup #1", "ob_setup_1.2": "Modified Russian setup #2", |
||||
"ob_setup_1.3": "Modified Russian setup #3", "ob_setup_1.4": "Modified Russian setup #4", |
||||
"ob_setup_1.5": "Modified Russian setup #5", |
||||
"ob_note_1.1": "Modified Russian note #1", |
||||
"ob_vehicles_1": "T-34/85", |
||||
"ob_ordnance_1": "82mm BM obr. 37", |
||||
"ob_setup_2.1": "Modified German setup #1", |
||||
"ob_note_2.1": "Modified German note #1", "ob_note_2.2": "Modified German note #2", |
||||
"ob_note_2.3": "Modified German note #3", "ob_note_2.4": "Modified German note #4", |
||||
"ob_note_2.5": "Modified German note #5", |
||||
"pf": "Panzerfaust", "atmm": "Anti-Tank Magnetic Mines", |
||||
"ob_vehicles_2": "PzKpfw VG", |
||||
"ob_ordnance_2": "3.7cm PaK 35/36", |
||||
} ) |
||||
# update the VASL scenario again (nothing should change) |
||||
updated_vsav_data = _update_vsav( temp_file.name, {} ) |
||||
assert updated_vsav_data == b"No changes." |
||||
|
||||
# run the test against all versions of VASSAL+VASL |
||||
_run_tests( webapp, monkeypatch, do_test, True ) |
||||
|
||||
# --------------------------------------------------------------------- |
||||
|
||||
@pytest.mark.skipif( not pytest.config.option.vasl_mods, reason="--vasl-mods not specified" ) #pylint: disable=no-member |
||||
@pytest.mark.skipif( not pytest.config.option.vassal, reason="--vassal not specified" ) #pylint: disable=no-member |
||||
@pytest.mark.skipif( pytest.config.option.short_tests, reason="--short-tests specified" ) #pylint: disable=no-member |
||||
def test_latw_autocreate( webapp, webdriver, monkeypatch ): |
||||
"""Test auto-creation of LATW labels.""" |
||||
|
||||
# initialize |
||||
monkeypatch.setitem( webapp.config, "DATA_DIR", REAL_DATA_DIR ) |
||||
init_webapp( webapp, webdriver, vsav_persistence=1 ) |
||||
|
||||
# NOTE: We disable this for speed, since we don't care about label positioning. |
||||
monkeypatch.setitem( webapp.config, "DISABLE_UPDATE_VSAV_SCREENSHOTS", True ) |
||||
|
||||
# NOTE: We're only interested in what happens with the LATW labels, we ignore everything else. |
||||
ignore_labels = [ "scenario", "players", "victory_conditions" ] |
||||
|
||||
def do_test(): #pylint: disable=missing-docstring |
||||
|
||||
# check the VASL scenario |
||||
fname = os.path.join( os.path.split(__file__)[0], "fixtures/update-vsav/empty.vsav" ) |
||||
vassal_shim = VassalShim() |
||||
vsav_dump = vassal_shim.dump_scenario( fname ) |
||||
_check_vsav_dump( vsav_dump, {}, ignore_labels ) |
||||
|
||||
# update the scenario (German/Russian, no date) |
||||
load_scenario_params( { "scenario": { "PLAYER_1": "german", "PLAYER_2": "russian", "SCENARIO_DATE": "" } } ) |
||||
updated_vsav_dump = _update_vsav_and_dump( fname, { "created": 3 } ) |
||||
_check_vsav_dump( updated_vsav_dump, { |
||||
# nb: no LATW labels should have been created |
||||
}, ignore_labels ) |
||||
|
||||
# update the scenario (German/Russian, OCT/43) |
||||
load_scenario_params( { |
||||
"scenario": { "PLAYER_1": "german", "PLAYER_2": "russian", "SCENARIO_DATE": "10/01/1943" } |
||||
} ) |
||||
updated_vsav_dump = _update_vsav_and_dump( fname, { "created": 4 } ) |
||||
_check_vsav_dump( updated_vsav_dump, { |
||||
"pf": "Panzerfaust", |
||||
}, ignore_labels ) |
||||
|
||||
# update the scenario (German/Russian, JAN/44) |
||||
load_scenario_params( { |
||||
"scenario": { "PLAYER_1": "german", "PLAYER_2": "russian", "SCENARIO_DATE": "01/01/1944" } |
||||
} ) |
||||
updated_vsav_dump = _update_vsav_and_dump( fname, { "created": 5 } ) |
||||
_check_vsav_dump( updated_vsav_dump, { |
||||
"pf": "Panzerfaust", "atmm": "ATMM check:", |
||||
}, ignore_labels ) |
||||
|
||||
# update the scenario (British/American, no date) |
||||
load_scenario_params( { "scenario": { "PLAYER_1": "british", "PLAYER_2": "american", "SCENARIO_DATE": "" } } ) |
||||
updated_vsav_dump = _update_vsav_and_dump( fname, { "created": 3 } ) |
||||
_check_vsav_dump( updated_vsav_dump, { |
||||
# nb: no LATW labels should have been created |
||||
}, ignore_labels ) |
||||
|
||||
# update the scenario (British/American, DEC/45) |
||||
load_scenario_params( { |
||||
"scenario": { "PLAYER_1": "british", "PLAYER_2": "american", "SCENARIO_DATE": "12/31/1945" } |
||||
} ) |
||||
updated_vsav_dump = _update_vsav_and_dump( fname, { "created": 3 } ) |
||||
_check_vsav_dump( updated_vsav_dump, { |
||||
# nb: no LATW labels should have been created |
||||
}, ignore_labels ) |
||||
|
||||
# run the test |
||||
# NOTE: We're testing the logic in the front/back-ends that determine whether LATW labels |
||||
# get created/updated/deleted, not the interaction with VASSAL, so we don't need to test |
||||
# against every VASSAL+VASL combination (although we can, if we want, but it'll be slow!) |
||||
_run_tests( webapp, monkeypatch, do_test, False ) |
||||
|
||||
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
||||
|
||||
@pytest.mark.skipif( not pytest.config.option.vasl_mods, reason="--vasl-mods not specified" ) #pylint: disable=no-member |
||||
@pytest.mark.skipif( not pytest.config.option.vassal, reason="--vassal not specified" ) #pylint: disable=no-member |
||||
@pytest.mark.skipif( pytest.config.option.short_tests, reason="--short-tests specified" ) #pylint: disable=no-member |
||||
def test_latw_update( webapp, webdriver, monkeypatch ): |
||||
"""Test updating of LATW labels.""" |
||||
|
||||
# initialize |
||||
monkeypatch.setitem( webapp.config, "DATA_DIR", REAL_DATA_DIR ) |
||||
init_webapp( webapp, webdriver, vsav_persistence=1 ) |
||||
|
||||
# NOTE: We disable this for speed, since we don't care about label positioning. |
||||
monkeypatch.setitem( webapp.config, "DISABLE_UPDATE_VSAV_SCREENSHOTS", True ) |
||||
|
||||
# NOTE: We're only interested in what happens with the LATW labels, we ignore everything else. |
||||
ignore_labels = [ "scenario", "players", "victory_conditions" ] |
||||
|
||||
def do_test(): #pylint: disable=missing-docstring |
||||
|
||||
# check the VASL scenario |
||||
fname = os.path.join( os.path.split(__file__)[0], "fixtures/update-vsav/latw.vsav" ) |
||||
vassal_shim = VassalShim() |
||||
vsav_dump = vassal_shim.dump_scenario( fname ) |
||||
_check_vsav_dump( vsav_dump, { |
||||
"psk": "Panzerschrek", "atmm": "ATMM check:", # nb: the PF label has no snippet ID |
||||
"mol-p": "TH#", # nb: the MOL label has no snippet ID |
||||
"piat": "TH#", |
||||
"baz": "Bazooka", |
||||
}, ignore_labels ) |
||||
|
||||
# update the scenario (German/Russian, no date) |
||||
load_scenario_params( { "scenario": { "PLAYER_1": "german", "PLAYER_2": "russian", "SCENARIO_DATE": "" } } ) |
||||
updated_vsav_dump = _update_vsav_and_dump( fname, { "created": 3, "updated": 2, "deleted": 2 } ) |
||||
_check_vsav_dump( updated_vsav_dump, { |
||||
"pf": "Panzerfaust", "psk": "Panzerschrek", "atmm": "ATMM check:", # nb: the PF label now has a snippet ID |
||||
"mol": "Kindling Attempt:", "mol-p": "TH#", # nb: the MOL label now has a snippet ID |
||||
# nb: the PIAT and BAZ labels are now gone |
||||
}, ignore_labels ) |
||||
|
||||
# update the scenario (British/American, DEC/1943) |
||||
load_scenario_params( { |
||||
"scenario": { "PLAYER_1": "british", "PLAYER_2": "american", "SCENARIO_DATE": "12/31/1943" } |
||||
} ) |
||||
updated_vsav_dump = _update_vsav_and_dump( fname, { "created": 3, "updated": 1, "deleted": 3 } ) |
||||
_check_vsav_dump( updated_vsav_dump, { |
||||
# nb: the PSK/ATMM and MOL-P label are now gone |
||||
"piat": "TH#", |
||||
"baz": "Bazooka ('43)", # nb: this has changed from '45 |
||||
}, ignore_labels ) |
||||
|
||||
# run the test |
||||
# NOTE: We're testing the logic in the front/back-ends that determine whether LATW labels |
||||
# get created/updated/deleted, not the interaction with VASSAL, so we don't need to test |
||||
# against every VASSAL+VASL combination (although we can, if we want, but it'll be slow!) |
||||
_run_tests( webapp, monkeypatch, do_test, False ) |
||||
|
||||
# --------------------------------------------------------------------- |
||||
|
||||
@pytest.mark.skipif( not pytest.config.option.vasl_mods, reason="--vasl-mods not specified" ) #pylint: disable=no-member |
||||
@pytest.mark.skipif( not pytest.config.option.vassal, reason="--vassal not specified" ) #pylint: disable=no-member |
||||
@pytest.mark.skipif( pytest.config.option.short_tests, reason="--short-tests specified" ) #pylint: disable=no-member |
||||
def test_dump_vsav( webapp, webdriver, monkeypatch ): |
||||
"""Test dumping a scenario.""" |
||||
|
||||
# initialize |
||||
init_webapp( webapp, webdriver ) |
||||
|
||||
def do_test(): #pylint: disable=missing-docstring |
||||
|
||||
# dump the VASL scenario |
||||
fname = os.path.join( os.path.split(__file__)[0], "fixtures/dump-vsav/labels.vsav" ) |
||||
vassal_shim = VassalShim() |
||||
vsav_dump = vassal_shim.dump_scenario( fname ) |
||||
|
||||
# check the result |
||||
fname = change_extn( fname, ".txt" ) |
||||
expected = open( fname, "r" ).read() |
||||
assert vsav_dump == expected |
||||
|
||||
# run the test against all versions of VASSAL+VASL |
||||
_run_tests( webapp, monkeypatch, do_test, True ) |
||||
|
||||
# --------------------------------------------------------------------- |
||||
|
||||
@pytest.mark.skipif( not pytest.config.option.vasl_mods, reason="--vasl-mods not specified" ) #pylint: disable=no-member |
||||
@pytest.mark.skipif( not pytest.config.option.vassal, reason="--vassal not specified" ) #pylint: disable=no-member |
||||
@pytest.mark.skipif( pytest.config.option.short_tests, reason="--short-tests specified" ) #pylint: disable=no-member |
||||
def test_legacy_labels( webapp, webdriver, monkeypatch ): |
||||
"""Test detection and updating of legacy labels.""" |
||||
|
||||
# initialize |
||||
monkeypatch.setitem( webapp.config, "DATA_DIR", REAL_DATA_DIR ) |
||||
init_webapp( webapp, webdriver, vsav_persistence=1, scenario_persistence=1 ) |
||||
|
||||
# NOTE: We disable this for speed, since we don't care about label positioning. |
||||
monkeypatch.setitem( webapp.config, "DISABLE_UPDATE_VSAV_SCREENSHOTS", True ) |
||||
|
||||
def do_test(): #pylint: disable=missing-docstring |
||||
|
||||
# dump the VASL scenario |
||||
# NOTE: We implemented snippet ID's in v0.5, this scenario is the "Hill 621" example from v0.4. |
||||
fname = os.path.join( os.path.split(__file__)[0], "fixtures/update-vsav/hill621-legacy.vsav" ) |
||||
vassal_shim = VassalShim() |
||||
vsav_dump = vassal_shim.dump_scenario( fname ) |
||||
labels = _get_vsav_labels( vsav_dump ) |
||||
assert len( [ lbl for lbl in labels if "vasl-templates:id" not in lbl ] ) == 20 |
||||
assert len( [ lbl for lbl in labels if "vasl-templates:id" in lbl ] ) == 0 #pylint: disable=len-as-condition |
||||
|
||||
# load the scenario into the UI and update the VSAV |
||||
fname2 = change_extn( fname, ".json" ) |
||||
saved_scenario = json.load( open( fname2, "r" ) ) |
||||
load_scenario( saved_scenario ) |
||||
updated_vsav_dump = _update_vsav_and_dump( fname, { "created": 1, "updated": 20 } ) |
||||
|
||||
# check the results |
||||
# nb: the update process should create 1 new label (the "Download from MMP" scenario note) |
||||
labels = _get_vsav_labels( updated_vsav_dump ) |
||||
assert len( [ lbl for lbl in labels if "vasl-templates:id" not in lbl ] ) == 0 #pylint: disable=len-as-condition |
||||
assert len( [ lbl for lbl in labels if "vasl-templates:id" in lbl ] ) == 21 |
||||
_check_vsav_dump( updated_vsav_dump, { |
||||
"scenario": "Near Minsk", |
||||
"players": re.compile( r"Russian:.*German:" ), |
||||
"victory_conditions": "five Level 3 hill hexes", |
||||
"ssr": re.compile( r"no wind at start.*must take a TC" ), |
||||
"scenario_note.1": "Download the scenario card", |
||||
"ob_setup_1.1": "whole hex of Board 3", |
||||
"ob_setup_1.2": "Enter on Turn 2", "ob_setup_1.3": "Enter on Turn 5", |
||||
"ob_vehicles_1": re.compile( r"T-34 M43.*SU-152.*SU-122.*ZIS-5" ), |
||||
"ob_setup_2.1": "whole hex of Board 4", |
||||
"ob_setup_2.2": "Enter on Turn 1", "ob_setup_2.3": "Enter on Turn 2", "ob_setup_2.4": "Enter on Turn 4", |
||||
"ob_setup_2.5": "Enter on Turn 5", "ob_setup_2.6": "Enter on Turn 8", |
||||
"ob_note_2.1": "80+mm Battalion Mortar", |
||||
"ob_note_2.2": "100+mm OBA", |
||||
"ob_vehicles_2": re.compile( |
||||
r"PzKpfw IVH.*PzKpfw IIIN.*StuG IIIG \(L\).*StuH 42.*SPW 250/1.*SPW 251/1.*SPW 251/sMG" |
||||
), |
||||
"ob_ordnance_2": re.compile( r"7.5cm PaK 40.*5cm PaK 38" ), |
||||
"pf": "Panzerfaust", "atmm": "Anti-Tank Magnetic Mines", |
||||
} ) |
||||
|
||||
# run the test |
||||
_run_tests( webapp, monkeypatch, do_test, False ) |
||||
|
||||
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
||||
|
||||
@pytest.mark.skipif( not pytest.config.option.vasl_mods, reason="--vasl-mods not specified" ) #pylint: disable=no-member |
||||
@pytest.mark.skipif( not pytest.config.option.vassal, reason="--vassal not specified" ) #pylint: disable=no-member |
||||
@pytest.mark.skipif( pytest.config.option.short_tests, reason="--short-tests specified" ) #pylint: disable=no-member |
||||
def test_legacy_latw_labels( webapp, webdriver, monkeypatch ): |
||||
"""Test detection and updating of legacy LATW labels.""" |
||||
|
||||
# initialize |
||||
monkeypatch.setitem( webapp.config, "DATA_DIR", REAL_DATA_DIR ) |
||||
init_webapp( webapp, webdriver, vsav_persistence=1, scenario_persistence=1 ) |
||||
|
||||
# NOTE: We disable this for speed, since we don't care about label positioning. |
||||
monkeypatch.setitem( webapp.config, "DISABLE_UPDATE_VSAV_SCREENSHOTS", True ) |
||||
|
||||
def do_test(): #pylint: disable=missing-docstring |
||||
|
||||
# dump the VASL scenario |
||||
# NOTE: This scenario contains LATW labels created using v0.4 i.e. they have no snippet ID's. |
||||
fname = os.path.join( os.path.split(__file__)[0], "fixtures/update-vsav/latw-legacy.vsav" ) |
||||
vassal_shim = VassalShim() |
||||
vsav_dump = vassal_shim.dump_scenario( fname ) |
||||
labels = _get_vsav_labels( vsav_dump ) |
||||
assert len( [ lbl for lbl in labels if "vasl-templates:id" not in lbl ] ) == 8 |
||||
assert len( [ lbl for lbl in labels if "vasl-templates:id" in lbl ] ) == 0 #pylint: disable=len-as-condition |
||||
|
||||
# NOTE: We're only interested in what happens with the LATW labels, ignore everything else |
||||
ignore_labels = [ "scenario", "players", "victory_conditions" ] |
||||
|
||||
# update the VSAV (all LATW are active) |
||||
load_scenario_params( { |
||||
"scenario": { "PLAYER_1": "german", "PLAYER_2": "russian", "SCENARIO_DATE": "12/31/1945" } |
||||
} ) |
||||
updated_vsav_dump = _update_vsav_and_dump( fname, { "created": 3, "updated": 5 } ) |
||||
_check_vsav_dump( updated_vsav_dump, { |
||||
"pf": "Panzerfaust", "psk": "Panzerschrek", "atmm": "ATMM check:", |
||||
"mol": "Kindling Attempt:", "mol-p": "TH#", |
||||
}, ignore_labels ) |
||||
labels = _get_vsav_labels( updated_vsav_dump ) |
||||
# nb: the legacy labels left in place: the scenario comment, and the PIAT/BAZ labels |
||||
assert len( [ lbl for lbl in labels if "vasl-templates:id" not in lbl ] ) == 3 |
||||
|
||||
# update the VSAV (all LATW are active) |
||||
load_scenario_params( { |
||||
"scenario": { "PLAYER_1": "british", "PLAYER_2": "american", "SCENARIO_DATE": "12/31/1945" } |
||||
} ) |
||||
updated_vsav_dump = _update_vsav_and_dump( fname, { "created": 3, "updated": 2 } ) |
||||
_check_vsav_dump( updated_vsav_dump, { |
||||
"piat": "PIAT", |
||||
"baz": "Bazooka ('45)", |
||||
}, ignore_labels ) |
||||
labels = _get_vsav_labels( updated_vsav_dump ) |
||||
# nb: the legacy labels left in place: the scenario comment, the PF/PSK/ATMM and MOL/MOL-P labels |
||||
assert len( [ lbl for lbl in labels if "vasl-templates:id" not in lbl ] ) == 6 |
||||
|
||||
# update the VSAV (some LATW are active) |
||||
load_scenario_params( { "scenario": { "PLAYER_1": "german", "PLAYER_2": "russian", "SCENARIO_DATE": "" } } ) |
||||
updated_vsav_dump = _update_vsav_and_dump( fname, { "created": 3, "updated": 5 } ) |
||||
_check_vsav_dump( updated_vsav_dump, { |
||||
"pf": "Panzerfaust", "psk": "Panzerschrek", "atmm": "ATMM check:", |
||||
"mol": "Kindling Attempt:", "mol-p": "TH#", |
||||
}, ignore_labels ) |
||||
labels = _get_vsav_labels( updated_vsav_dump ) |
||||
# nb: the legacy labels left in place: the scenario comment, the PIAT/BAZ labels |
||||
assert len( [ lbl for lbl in labels if "vasl-templates:id" not in lbl ] ) == 3 |
||||
|
||||
# update the VSAV (some LATW are active) |
||||
load_scenario_params( { "scenario": { "PLAYER_1": "british", "PLAYER_2": "american", "SCENARIO_DATE": "" } } ) |
||||
updated_vsav_dump = _update_vsav_and_dump( fname, { "created": 3, "updated": 2 } ) |
||||
_check_vsav_dump( updated_vsav_dump, { |
||||
"piat": "PIAT", |
||||
"baz": "Bazooka", |
||||
}, ignore_labels ) |
||||
labels = _get_vsav_labels( updated_vsav_dump ) |
||||
# nb: the legacy labels left in place: the scenario comment, the PF/PSK/ATMM, MOL/MOL-P and BAZ labels |
||||
assert len( [ lbl for lbl in labels if "vasl-templates:id" not in lbl ] ) == 6 |
||||
|
||||
# run the test |
||||
_run_tests( webapp, monkeypatch, do_test, False ) |
||||
|
||||
# --------------------------------------------------------------------- |
||||
|
||||
def _run_tests( webapp, monkeypatch, func, test_all ): |
||||
"""Run the test function for each combination of VASSAL + VASL. |
||||
|
||||
This is, of course, going to be insanely slow, since we need to spin up a JVM |
||||
and initialize VASSAL/VASL each time :-/ |
||||
""" |
||||
|
||||
# locate all VASL modules |
||||
vasl_mods_dir = pytest.config.option.vasl_mods #pylint: disable=no-member |
||||
fspec = os.path.join( vasl_mods_dir, "*.vmod" ) |
||||
vasl_mods = glob.glob( fspec ) |
||||
|
||||
# locate all VASSAL engines |
||||
vassal_engines = [] |
||||
vassal_dir = pytest.config.option.vassal #pylint: disable=no-member |
||||
for root,_,fnames in os.walk( vassal_dir ): |
||||
for fname in fnames: |
||||
if fname == "Vengine.jar": |
||||
vassal_engines.append( root ) |
||||
|
||||
# check if we want to test all VASSAL+VASL combinations (nb: if not, we test against only one combination, |
||||
# and since they all should give the same results, it doesn't matter which one. |
||||
if not test_all: |
||||
vasl_mods = [ random.choice( vasl_mods ) ] |
||||
vassal_engines = [ random.choice( vassal_engines ) ] |
||||
|
||||
# run the test for each VASSAL+VASL |
||||
for vassal_engine in vassal_engines: |
||||
monkeypatch.setitem( webapp.config, "VASSAL_DIR", vassal_engine ) |
||||
for vasl_mod in vasl_mods: |
||||
monkeypatch.setitem( webapp.config, "VASL_MOD", vasl_mod ) |
||||
func() |
||||
|
||||
# --------------------------------------------------------------------- |
||||
|
||||
def _update_vsav( fname, expected ): |
||||
"""Update a VASL scenario.""" |
||||
|
||||
# read the VSAV data |
||||
vsav_data = open( fname, "rb" ).read() |
||||
|
||||
# send the VSAV data to the front-end to be updated |
||||
set_stored_msg( "_vsav-persistence_", base64.b64encode( vsav_data ).decode( "utf-8" ) ) |
||||
_ = set_stored_msg_marker( "_last-info_" ) |
||||
_ = set_stored_msg_marker( "_last-warning_" ) |
||||
select_menu_option( "update_vsav" ) |
||||
|
||||
# wait for the results to come back |
||||
wait_for( 2, lambda: get_stored_msg( "_vsav-persistence_" ) == "" ) # nb: wait for the front-end to receive the data |
||||
timeout = 120 if os.name == "nt" else 60 |
||||
wait_for( timeout, lambda: get_stored_msg( "_vsav-persistence_" ) != "" ) # nb: wait for the updated data to arrive |
||||
updated_vsav_data = get_stored_msg( "_vsav-persistence_" ) |
||||
if updated_vsav_data.startswith( "ERROR: " ): |
||||
raise RuntimeError( updated_vsav_data ) |
||||
updated_vsav_data = base64.b64decode( updated_vsav_data ) |
||||
|
||||
# parse the VASSAL shim report |
||||
if expected: |
||||
report = {} |
||||
msg = get_stored_msg( "_last-warning_" if "deleted" in expected else "_last-info_" ) |
||||
assert "The VASL scenario was updated:" in msg |
||||
for mo2 in re.finditer( "<li>([^<]+)", msg ): |
||||
mo3 = re.search( r"^(\d+) labels? (were|was) ([a-z]+)", mo2.group(1) ) |
||||
report[ mo3.group(3) ] = int( mo3.group(1) ) |
||||
assert report == expected |
||||
else: |
||||
assert "No changes were made" in get_stored_msg( "_last-info_" ) |
||||
|
||||
return updated_vsav_data |
||||
|
||||
def _update_vsav_and_dump( fname, expected ): |
||||
"""Update a VASL scenario and dump the result.""" |
||||
|
||||
# update the VASL |
||||
updated_vsav_data = _update_vsav( fname, expected ) |
||||
|
||||
# dump the updated VSAV |
||||
with TempFile() as temp_file: |
||||
temp_file.write( updated_vsav_data ) |
||||
temp_file.close() |
||||
vassal_shim = VassalShim() |
||||
updated_vsav_dump = vassal_shim.dump_scenario( temp_file.name ) |
||||
return updated_vsav_dump |
||||
|
||||
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
||||
|
||||
def _check_vsav_dump( vsav_dump, expected, ignore=None ): |
||||
""""Check that a VASL scenario dump contains what we expect.""" |
||||
|
||||
# extract the information of interest from the dump |
||||
labels = {} |
||||
for label in _get_vsav_labels(vsav_dump): |
||||
mo2 = re.search( r"<!-- vasl-templates:id (.*?) -->", label, re.DOTALL ) |
||||
if not mo2: |
||||
continue # nb: this is not one of ours |
||||
snippet_id = mo2.group( 1 ) |
||||
if snippet_id.startswith( "extras/" ): |
||||
continue |
||||
labels[snippet_id] = label |
||||
|
||||
# compare what we extracted from the dump with what's expected |
||||
for snippet_id in expected: |
||||
if isinstance( expected[snippet_id], typing.re.Pattern ): |
||||
rc = expected[snippet_id].search( labels[snippet_id] ) is not None |
||||
else: |
||||
assert isinstance( expected[snippet_id], str ) |
||||
rc = expected[snippet_id] in labels[snippet_id] |
||||
if not rc: |
||||
print( "Can't find {} in label: {}".format( expected[snippet_id], labels[snippet_id] ) ) |
||||
assert False |
||||
del labels[snippet_id] |
||||
|
||||
# check for unexpected extra labels in the VASL scenario |
||||
if ignore: |
||||
labels = [ lbl for lbl in labels if lbl not in ignore ] |
||||
if labels: |
||||
for snippet_id in labels: |
||||
print( "Extra label in the VASL scenario: {}".format( snippet_id ) ) |
||||
assert False |
||||
|
||||
def _get_vsav_labels( vsav_dump ): |
||||
"""Extract the labels from a VSAV dump.""" |
||||
matches = re.finditer( r"AddPiece: DynamicProperty/User-Labeled.*?- Map", vsav_dump, re.DOTALL ) |
||||
labels = [ mo.group() for mo in matches ] |
||||
regex = re.compile( r"<html>.*?</html>" ) |
||||
matches = [ regex.search(label) for label in labels ] |
||||
return [ mo.group() if mo else "<???>" for mo in matches ] |
@ -0,0 +1,117 @@ |
||||
""" Miscellaneous utilities. """ |
||||
|
||||
import os |
||||
import tempfile |
||||
import pathlib |
||||
|
||||
from selenium import webdriver |
||||
from PIL import Image, ImageChops |
||||
|
||||
from vasl_templates.webapp import app |
||||
|
||||
# --------------------------------------------------------------------- |
||||
|
||||
class TempFile: |
||||
"""Manage a temp file that can be closed while it's still being used.""" |
||||
|
||||
def __init__( self, mode="wb", extn=None ): |
||||
self.mode = mode |
||||
self.extn = extn |
||||
self.temp_file = None |
||||
self.name = None |
||||
|
||||
def __enter__( self ): |
||||
"""Allocate a temp file.""" |
||||
self.temp_file = tempfile.NamedTemporaryFile( mode=self.mode, suffix=self.extn, delete=False ) |
||||
self.name = self.temp_file.name |
||||
return self |
||||
|
||||
def __exit__( self, exc_type, exc_val, exc_tb ): |
||||
"""Clean up the temp file.""" |
||||
self.close() |
||||
os.unlink( self.temp_file.name ) |
||||
|
||||
def write( self, data ): |
||||
"""Write data to the temp file.""" |
||||
self.temp_file.write( data ) |
||||
|
||||
def close( self ): |
||||
"""Close the temp file.""" |
||||
self.temp_file.close() |
||||
|
||||
# --------------------------------------------------------------------- |
||||
|
||||
class HtmlScreenshots: |
||||
"""Generate preview screenshots of HTML.""" |
||||
|
||||
def __init__( self ): |
||||
self.webdriver = None |
||||
|
||||
def __enter__( self ): |
||||
"""Initialize the HTML screenshot engine.""" |
||||
webdriver_path = app.config.get( "WEBDRIVER_PATH" ) |
||||
if not webdriver_path: |
||||
raise SimpleError( "No webdriver has been configured." ) |
||||
# NOTE: If we are being run on Windows without a console (e.g. the frozen PyQt desktop app), |
||||
# Selenium will launch the webdriver in a visible DOS box :-( There's no way to turn this off, |
||||
# but it can be disabled by modifying the Selenium source code. Find the subprocess.Popen() call |
||||
# in $/site-packages/selenium/webdriver/common/service.py and add the following parameter: |
||||
# creationflags = 0x8000000 # win32process.CREATE_NO_WINDOW |
||||
# It's pretty icky to have to do this, but since we're in a virtualenv, it's not too bad... |
||||
kwargs = { "executable_path": webdriver_path } |
||||
if "chromedriver" in webdriver_path: |
||||
options = webdriver.ChromeOptions() |
||||
options.set_headless( headless=True ) |
||||
kwargs["chrome_options"] = options |
||||
self.webdriver = webdriver.Chrome( **kwargs ) |
||||
elif "geckodriver" in webdriver_path: |
||||
options = webdriver.FirefoxOptions() |
||||
options.set_headless( headless=True ) |
||||
kwargs["firefox_options"] = options |
||||
kwargs["log_path"] = app.config.get( "GECKODRIVER_LOG", |
||||
os.path.join( tempfile.gettempdir(), "geckodriver.log" ) |
||||
) |
||||
self.webdriver = webdriver.Firefox( **kwargs ) |
||||
else: |
||||
raise SimpleError( "Can't identify webdriver: {}".format( webdriver_path ) ) |
||||
return self |
||||
|
||||
def __exit__( self, exc_type, exc_val, exc_tb ): |
||||
"""Clean up.""" |
||||
if self.webdriver: |
||||
self.webdriver.quit() |
||||
|
||||
def get_screenshot( self, html, window_size ): |
||||
"""Get a preview screenshot of the specified HTML.""" |
||||
|
||||
self.webdriver.set_window_size( window_size[0], window_size[1] ) |
||||
with TempFile( extn=".html", mode="w" ) as html_tempfile: |
||||
|
||||
# take a screenshot of the HTML |
||||
# NOTE: We could do some funky Javascript stuff to load the browser directly from the string, |
||||
# but using a temp file is straight-forward and pretty much guaranteed to work :-/ |
||||
html_tempfile.write( html ) |
||||
html_tempfile.close() |
||||
self.webdriver.get( "file://{}".format( html_tempfile.name ) ) |
||||
with TempFile( extn=".png" ) as screenshot_tempfile: |
||||
screenshot_tempfile.close() |
||||
self.webdriver.save_screenshot( screenshot_tempfile.name ) |
||||
img = Image.open( screenshot_tempfile.name ) |
||||
|
||||
# trim the screenshot (nb: we assume a white background) |
||||
bgd = Image.new( img.mode, img.size, (255,255,255,255) ) |
||||
diff = ImageChops.difference( img, bgd ) |
||||
bbox = diff.getbbox() |
||||
return img.crop( bbox ) |
||||
|
||||
# --------------------------------------------------------------------- |
||||
|
||||
def change_extn( fname, extn ): |
||||
"""Change a filename's extension.""" |
||||
return pathlib.Path( fname ).with_suffix( extn ) |
||||
|
||||
# --------------------------------------------------------------------- |
||||
|
||||
class SimpleError( Exception ): |
||||
"""Represents a simple error that doesn't require a stack trace (e.g. bad configuration).""" |
||||
pass |
@ -0,0 +1,349 @@ |
||||
""" Webapp handlers. """ |
||||
# Kathmandu, Nepal (NOV/18). |
||||
|
||||
import sys |
||||
import os |
||||
import subprocess |
||||
import traceback |
||||
import json |
||||
import re |
||||
import logging |
||||
import base64 |
||||
import time |
||||
import xml.etree.cElementTree as ET |
||||
|
||||
from flask import request |
||||
|
||||
from vasl_templates.webapp import app |
||||
from vasl_templates.webapp.config.constants import BASE_DIR, IS_FROZEN |
||||
from vasl_templates.webapp.utils import TempFile, HtmlScreenshots, SimpleError |
||||
|
||||
_logger = logging.getLogger( "update_vsav" ) |
||||
|
||||
SUPPORTED_VASSAL_VERSIONS = [ "3.2.15" ,"3.2.16", "3.2.17" ] |
||||
SUPPORTED_VASSAL_VERSIONS_DISPLAY = "3.2.15-.17" |
||||
|
||||
# --------------------------------------------------------------------- |
||||
|
||||
@app.route( "/update-vsav", methods=["POST"] ) |
||||
def update_vsav(): #pylint: disable=too-many-statements |
||||
"""Update labels in a VASL scenario file.""" |
||||
|
||||
# parse the request |
||||
start_time = time.time() |
||||
vsav_data = request.json[ "vsav_data" ] |
||||
vsav_filename = request.json[ "filename" ] |
||||
snippets = request.json[ "snippets" ] |
||||
|
||||
# update the VASL scenario file |
||||
try: |
||||
|
||||
# get the VSAV data (we do this inside the try block so that the user gets shown |
||||
# a proper error dialog if there's a problem decoding the base64 data) |
||||
vsav_data = base64.b64decode( vsav_data ) |
||||
_logger.info( "Updating VSAV (#bytes=%d): %s", len(vsav_data), vsav_filename ) |
||||
|
||||
with TempFile() as input_file: |
||||
# save the VSAV data in a temp file |
||||
input_file.write( vsav_data ) |
||||
input_file.close() |
||||
fname = app.config.get( "UPDATE_VSAV_INPUT" ) # nb: for diagnosing problems |
||||
if fname: |
||||
_logger.debug( "Saving a copy of the VSAV data: %s", fname ) |
||||
with open( fname, "wb" ) as fp: |
||||
fp.write( vsav_data ) |
||||
with TempFile() as snippets_file: |
||||
# save the snippets in a temp file |
||||
xml = _save_snippets( snippets, snippets_file ) |
||||
snippets_file.close() |
||||
fname = app.config.get( "UPDATE_VSAV_SNIPPETS" ) # nb: for diagnosing problems |
||||
if fname: |
||||
_logger.debug( "Saving a copy of the snippets: %s", fname ) |
||||
with open( fname, "wb" ) as fp: |
||||
ET.ElementTree( xml ).write( fp ) |
||||
# run the VASSAL shim to update the VSAV file |
||||
with TempFile() as output_file, TempFile() as report_file: |
||||
output_file.close() |
||||
report_file.close() |
||||
vassal_shim = VassalShim() |
||||
vassal_shim.update_scenario( |
||||
input_file.name, snippets_file.name, output_file.name, report_file.name |
||||
) |
||||
# read the updated VSAV data |
||||
with open( output_file.name, "rb" ) as fp: |
||||
vsav_data = fp.read() |
||||
fname = app.config.get( "UPDATE_VSAV_RESULT" ) # nb: for diagnosing problems |
||||
if fname: |
||||
_logger.debug( "Saving a copy of the update VSAV: %s", fname ) |
||||
with open( app.config.get("UPDATE_VSAV_RESULT"), "wb" ) as fp: |
||||
fp.write( vsav_data ) |
||||
# read the report |
||||
label_report = _parse_label_report( report_file.name ) |
||||
except VassalShimError as ex: |
||||
_logger.error( "VASSAL shim error: rc=%d", ex.retcode ) |
||||
if ex.retcode != 0: |
||||
return json.dumps( { |
||||
"error": "Unexpected return code from the VASSAL shim: {}".format( ex.retcode ), |
||||
"stdout": ex.stdout, |
||||
"stderr": ex.stderr, |
||||
} ) |
||||
return json.dumps( { |
||||
"error": "Unexpected error output from the VASSAL shim.", |
||||
"stdout": ex.stdout, |
||||
"stderr": ex.stderr, |
||||
} ) |
||||
except subprocess.TimeoutExpired: |
||||
return json.dumps( { |
||||
"error": "<p>The updater took too long to run, please try again." \ |
||||
"<p>If this problem persists, try configuring a longer timeout." |
||||
} ) |
||||
except SimpleError as ex: |
||||
_logger.error( "VSAV update error: %s", ex ) |
||||
return json.dumps( { "error": str(ex) } ) |
||||
except Exception as ex: #pylint: disable=broad-except |
||||
_logger.error( "Unexpected VSAV update error: %s", ex ) |
||||
return json.dumps( { |
||||
"error": str(ex), |
||||
"stdout": traceback.format_exc(), |
||||
} ) |
||||
|
||||
# return the results |
||||
_logger.debug( "Updated the VSAV file OK: elapsed=%.3fs", time.time()-start_time ) |
||||
# NOTE: We adjust the recommended save filename to encourage users to not overwrite the original file :-/ |
||||
vsav_filename = os.path.split( vsav_filename )[1] |
||||
fname, extn = os.path.splitext( vsav_filename ) |
||||
return json.dumps( { |
||||
"vsav_data": base64.b64encode(vsav_data).decode( "utf-8" ), |
||||
"filename": fname+" (updated)" + extn, |
||||
"report": { |
||||
"was_modified": label_report["was_modified"], |
||||
"labels_created": len(label_report["created"]), |
||||
"labels_updated": len(label_report["updated"]), |
||||
"labels_deleted": len(label_report["deleted"]), |
||||
"labels_unchanged": len(label_report["unchanged"]), |
||||
}, |
||||
} ) |
||||
|
||||
def _save_snippets( snippets, fp ): |
||||
"""Save the snippets in a file. |
||||
|
||||
NOTE: We save the snippets as XML because Java :-/ |
||||
""" |
||||
|
||||
def get_html_size( snippet_id, html, window_size ): |
||||
"""Get the size of the specified HTML.""" |
||||
start_time = time.time() |
||||
img = html_screenshots.get_screenshot( html, window_size ) |
||||
elapsed_time = time.time() - start_time |
||||
width, height = img.size |
||||
_logger.debug( "Generated screenshot for %s (%.3fs): %dx%d", snippet_id, elapsed_time, width, height ) |
||||
return width, height |
||||
|
||||
def do_save_snippets( html_screenshots ): |
||||
"""Save the snippets.""" |
||||
|
||||
root = ET.Element( "snippets" ) |
||||
for key,val in snippets.items(): |
||||
|
||||
# add the next snippet |
||||
auto_create = "true" if val["auto_create"] else "false" |
||||
elem = ET.SubElement( root, "snippet", id=key, autoCreate=auto_create ) |
||||
elem.text = val["content"] |
||||
label_area = val.get( "label_area" ) |
||||
if label_area: |
||||
elem.set( "labelArea", label_area ) |
||||
|
||||
# add the raw content |
||||
elem2 = ET.SubElement( elem, "rawContent" ) |
||||
for node in val["raw_content"]: |
||||
ET.SubElement( elem2, "phrase" ).text = node |
||||
|
||||
# include the size of the snippet |
||||
if html_screenshots: |
||||
try: |
||||
# NOTE: Screenshots take significantly longer for larger window sizes. Since most of our snippets |
||||
# will be small, we first try with a smaller window, and switch to a larger one if necessary. |
||||
width, height = get_html_size( key, val["content"], (500,500) ) |
||||
if width >= 450 or height >= 450: |
||||
# NOTE: While it's tempting to set the browser window really large here, if the label ends up |
||||
# filling/overflowing the available space (e.g. because its width/height has been set to 100%), |
||||
# then the auto-created label will push any subsequent labels far down the map, possibly to |
||||
# somewhere unreachable. So, we set it somewhat more conservatively, so that if this happens, |
||||
# the user still has a chance to recover from it. Note that this doesn't mean that they can't |
||||
# have really large labels, it just affects the positioning of auto-created labels. |
||||
width, height = get_html_size( key, val["content"], (1500,1500) ) |
||||
# FUDGE! There's something weird going on in VASSAL e.g. "<table width=300>" gives us something |
||||
# very different to "<table style='width:300px;'>" :-/ Changing the font size also causes problems. |
||||
# The following fudging seems to give us something that's somewhat reasonable... :-/ |
||||
if re.search( r"width:\s*?\d+?px", val["content"] ): |
||||
width = int( width * 140 / 100 ) |
||||
elem.set( "width", str(width) ) |
||||
elem.set( "height", str(height) ) |
||||
except Exception as ex: #pylint: disable=broad-except |
||||
# NOTE: Don't let an error here stop the process. |
||||
logging.error( "Can't get snippet screenshot: %s", ex ) |
||||
logging.error( traceback.format_exc() ) |
||||
|
||||
ET.ElementTree( root ).write( fp ) |
||||
return root |
||||
|
||||
# save the snippets |
||||
if app.config.get( "DISABLE_UPDATE_VSAV_SCREENSHOTS" ): |
||||
return do_save_snippets( None ) |
||||
else: |
||||
with HtmlScreenshots() as html_screenshots: |
||||
return do_save_snippets( html_screenshots ) |
||||
|
||||
def _parse_label_report( fname ): |
||||
"""Read the label report generated by the VASSAL shim.""" |
||||
doc = ET.parse( fname ) |
||||
report = { |
||||
"was_modified": doc.getroot().attrib["wasModified"] == "true" |
||||
} |
||||
for action in doc.getroot(): |
||||
nodes = [] |
||||
for node in action: |
||||
nodes.append( { "id": node.attrib["id"] } ) |
||||
if "x" in node.attrib and "y" in node.attrib: |
||||
nodes[-1]["pos"] = ( node.attrib["x"], node.attrib["y"] ) |
||||
report[ action.tag ] = nodes |
||||
return report |
||||
|
||||
# --------------------------------------------------------------------- |
||||
|
||||
class VassalShim: |
||||
"""Provide access to VASSAL via the Java shim.""" |
||||
|
||||
def __init__( self ): |
||||
|
||||
# locate the VASSAL engine |
||||
vassal_dir = app.config.get( "VASSAL_DIR" ) |
||||
if not vassal_dir: |
||||
raise SimpleError( "The VASSAL installation directory has not been configured." ) |
||||
self.vengine_jar = None |
||||
for root,_,fnames in os.walk( vassal_dir ): |
||||
for fname in fnames: |
||||
if fname == "Vengine.jar": |
||||
self.vengine_jar = os.path.join( root, fname ) |
||||
break |
||||
if not self.vengine_jar: |
||||
raise SimpleError( "Can't find Vengine.jar: {}".format( vassal_dir ) ) |
||||
|
||||
# locate the boards |
||||
self.boards_dir = app.config.get( "BOARDS_DIR" ) |
||||
if not self.boards_dir: |
||||
raise SimpleError( "The VASL boards directory has not been configured." ) |
||||
if not os.path.isdir( self.boards_dir ): |
||||
raise SimpleError( "Can't find the VASL boards: {}".format( self.boards_dir ) ) |
||||
|
||||
# locate the VASL module |
||||
self.vasl_mod = app.config.get( "VASL_MOD" ) |
||||
if not self.vasl_mod: |
||||
raise SimpleError( "The VASL module has not been configured." ) |
||||
if not os.path.isfile( self.vasl_mod ): |
||||
raise SimpleError( "Can't find VASL module: {}".format( self.vasl_mod ) ) |
||||
|
||||
# locate the VASSAL shim JAR |
||||
if IS_FROZEN: |
||||
meipass = sys._MEIPASS #pylint: disable=no-member,protected-access |
||||
self.shim_jar = os.path.join( meipass, "vasl_templates/webapp/vassal-shim.jar" ) |
||||
else: |
||||
self.shim_jar = os.path.join( os.path.split(__file__)[0], "../../vassal-shim/release/vassal-shim.jar" ) |
||||
if not os.path.isfile( self.shim_jar ): |
||||
raise SimpleError( "Can't find the VASSAL shim JAR." ) |
||||
|
||||
def dump_scenario( self, fname ): |
||||
"""Dump a scenario file.""" |
||||
return self._run_vassal_shim( "dump", fname ) |
||||
|
||||
def update_scenario( self, vsav_fname, snippets_fname, output_fname, report_fname ): |
||||
"""Update a scenario file.""" |
||||
return self._run_vassal_shim( |
||||
"update", self.boards_dir, vsav_fname, snippets_fname, output_fname, report_fname |
||||
) |
||||
|
||||
def _run_vassal_shim( self, *args ): #pylint: disable=too-many-locals |
||||
"""Run the VASSAL shim.""" |
||||
|
||||
# prepare the command |
||||
java_path = app.config.get( "JAVA_PATH" ) |
||||
if not java_path: |
||||
java_path = "java" # nb: this must be in the PATH |
||||
class_path = app.config.get( "JAVA_CLASS_PATH" ) |
||||
if not class_path: |
||||
class_path = [ self.vengine_jar, self.shim_jar ] |
||||
class_path.append( os.path.split( self.shim_jar )[0] ) # nb: to find logback(-test).xml |
||||
if IS_FROZEN: |
||||
class_path.append( BASE_DIR ) # nb: also to find logback(-test).xml |
||||
sep = ";" if os.name == "nt" else ":" |
||||
class_path = sep.join( class_path ) |
||||
args2 = [ |
||||
java_path, "-classpath", class_path, "vassal_shim.Main", |
||||
args[0], self.vasl_mod |
||||
] |
||||
args2.extend( args[1:] ) |
||||
|
||||
# figure out how long to the let the VASSAL shim run |
||||
timeout = int( app.config.get( "VASSAL_SHIM_TIMEOUT", 120 ) ) |
||||
if timeout <= 0: |
||||
timeout = None |
||||
|
||||
# run the VASSAL shim |
||||
_logger.debug( "Running VASSAL shim (timeout=%s): %s", str(timeout), " ".join(args2) ) |
||||
start_time = time.time() |
||||
# NOTE: We can't use pipes to capture the output here when we're frozen on Windows ("invalid handle" errors), |
||||
# I suspect because we freeze the application using --noconsole, which causes problems when |
||||
# the child process tries to inherit handles. Capturing the output in temp files also fails (!), |
||||
# as does using subprocess.DEVNULL (!!!) Setting close_fds when calling Popen() also made no difference. |
||||
# The only thing that worked was removing "--noconsole" when freezing the application, but that causes |
||||
# a DOS box to appear when we are run :-/ |
||||
# However, we can also not specify any stdout/stderr, and since we don't actually check the output, |
||||
# we can get away with this, even if it is a bit icky :-/ However, if the VASSAL shim throws an error, |
||||
# we won't be able to show the stack trace, just a generic "VASSAL shim failed" message :-( |
||||
with TempFile() as buf1, TempFile() as buf2: |
||||
kwargs = {} |
||||
if not IS_FROZEN: |
||||
kwargs = { "stdout": buf1.temp_file, "stderr": buf2.temp_file } |
||||
if os.name == "nt": |
||||
# NOTE: Using CREATE_NO_WINDOW doesn't fix the problem of VASSAL's UI sometimes appearing, |
||||
# but it does hide the DOS box if the user has configured java.exe instead of javaw.exe. |
||||
kwargs["creationflags"] = 0x8000000 # nb: win32process.CREATE_NO_WINDOW |
||||
proc = subprocess.Popen( args2, **kwargs ) |
||||
try: |
||||
proc.wait( timeout ) |
||||
except subprocess.TimeoutExpired: |
||||
proc.kill() |
||||
raise |
||||
buf1.close() |
||||
stdout = open( buf1.name, "r", encoding="utf-8" ).read() |
||||
buf2.close() |
||||
stderr = open( buf2.name, "r", encoding="utf-8" ).read() |
||||
elapsed_time = time.time() - start_time |
||||
_logger.debug( "- Completed OK: %.3fs", elapsed_time ) |
||||
|
||||
# check the result |
||||
stderr = stderr.replace( "Warning: Could not get charToByteConverterClass!", "" ).strip() |
||||
# NOTE: VASSAL's internal representation of a scenario seems to be tightly coupled with its UI, |
||||
# which means that when we load a scenario, bits of the UI sometimes start appearing (although not always, |
||||
# presumably because there's a race between how fast we can make our changes and save the scenario |
||||
# vs. how fast the UI can start up :-/). When the UI does start to appear, it fails, presumably because |
||||
# we haven't performed the necessary startup incantations, and dumps a stack trace to stderr. |
||||
# The upshot is that the only thing we look for is an exit code of 0, which means that the VASSAL shim |
||||
# saved the scenario successfully and exited cleanly; any output on stderr means that some part |
||||
# of VASSAL barfed as it was trying to start up and can (hopefully) be safely ignored. |
||||
if stderr: |
||||
_logger.info( "VASSAL shim stderr output:\n%s", stderr ) |
||||
if proc.returncode != 0: |
||||
raise VassalShimError( proc.returncode, stdout, stderr ) |
||||
return stdout |
||||
|
||||
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
||||
|
||||
class VassalShimError( Exception ): |
||||
"""Represents an error returned by the VASSAL shim.""" |
||||
|
||||
def __init__( self, retcode, stdout, stderr ): |
||||
super().__init__() |
||||
self.retcode = retcode |
||||
self.stdout = stdout |
||||
self.stderr = stderr |
@ -0,0 +1,3 @@ |
||||
output/ |
||||
release/vassal-shim.properties |
||||
logback-test.xml |
@ -0,0 +1,27 @@ |
||||
# Define VASSAL_DIR in the command line arguments to point to the directory that contains Vengine.jar e.g.
|
||||
# make all VASSAL_DIR=...
|
||||
|
||||
SRC_DIR:=src
|
||||
DATA_DIR:=data
|
||||
OUTPUT_DIR:=output
|
||||
RELEASE_DIR:=release
|
||||
|
||||
JAVAC:=javac
|
||||
JAR:=jar
|
||||
CLASSPATH:=$(VASSAL_DIR)/Vengine.jar:$(OUTPUT_DIR)
|
||||
JAVAC_FLAGS:=-d $(OUTPUT_DIR) -classpath $(CLASSPATH) -sourcepath $(SRC_DIR) -Xlint:unchecked
|
||||
|
||||
all: init compile |
||||
|
||||
init: |
||||
mkdir -p $(OUTPUT_DIR)
|
||||
mkdir -p $(RELEASE_DIR)
|
||||
|
||||
compile: init |
||||
$(JAVAC) $(JAVAC_FLAGS) $(shell find $(SRC_DIR) -name '*.java')
|
||||
cp -r $(DATA_DIR) $(OUTPUT_DIR)
|
||||
$(JAR) cfe $(RELEASE_DIR)/vassal-shim.jar vassal_shim.Main -C $(OUTPUT_DIR) .
|
||||
|
||||
clean: |
||||
rm -r $(OUTPUT_DIR)
|
||||
rm -r $(RELEASE_DIR)
|
@ -0,0 +1,208 @@ |
||||
; This file defines VASL board names. |
||||
; nb: bd00-99 are handled automatically in the code. |
||||
|
||||
bd1a |
||||
bd1b |
||||
bd2a |
||||
bd2b |
||||
bd3a |
||||
bd3b |
||||
bd4a |
||||
bd4b |
||||
bd5a |
||||
bd5b |
||||
bd6a |
||||
bd6b |
||||
bd7a |
||||
bd7b |
||||
bd8a |
||||
bd8b |
||||
bd9a |
||||
bd9b |
||||
bd10z |
||||
bd17z |
||||
|
||||
bda |
||||
bdb |
||||
bdc |
||||
bdd |
||||
bdd1 |
||||
bdd2 |
||||
bdd3 |
||||
bdd4 |
||||
bdd5 |
||||
bdd6 |
||||
bdd7 |
||||
bde |
||||
bdf |
||||
bdg |
||||
bdh |
||||
bdp |
||||
bdq |
||||
bdr |
||||
bds |
||||
bdt |
||||
bdu |
||||
bdv |
||||
bdw |
||||
bdx |
||||
bdy |
||||
bdz |
||||
|
||||
bdABTF |
||||
bdASLNews |
||||
bdB&I |
||||
bdbatisse02 |
||||
bdbatisse03 |
||||
bdbatisse04 |
||||
bdBB |
||||
bdBDF |
||||
bdBF1 |
||||
bdBFPA |
||||
bdBFPB |
||||
bdBFPC |
||||
bdBFPD |
||||
bdBFPDW1a |
||||
bdBFPDW1b |
||||
bdBFPDW2a |
||||
bdBFPDW2b |
||||
bdBFPDW3a |
||||
bdBFPDW3b |
||||
bdBFPDW4a |
||||
bdBFPDW4b |
||||
bdBFPDW5a |
||||
bdBFPDW5b |
||||
bdBFPDW6a |
||||
bdBFPDW6b |
||||
bdBFPE |
||||
bdBFPF |
||||
bdBFPG |
||||
bdBFPH |
||||
bdBFPI |
||||
bdBFPJ |
||||
bdBFPK |
||||
bdBFPL |
||||
bdBFPM |
||||
bdBFPN |
||||
bdBFPO |
||||
bdBFPP |
||||
bdBFPQ |
||||
bdBFPR |
||||
bdBlank0 |
||||
bdBlank1 |
||||
bdBRT |
||||
bdBRV |
||||
bdCemHill |
||||
bdCH |
||||
bdCH1 |
||||
bdCH2 |
||||
bdCH3 |
||||
bdCH4 |
||||
bdCM |
||||
bdCRS |
||||
bdDaE |
||||
bdDBP |
||||
bdER |
||||
bdFB_CG2 |
||||
bdFB_CG3 |
||||
bdFB_NE |
||||
bdFB_NW |
||||
bdFB_SE |
||||
bdFB_SW |
||||
bdFC |
||||
bdfe3 |
||||
bdfe4 |
||||
bdfe5 |
||||
bdfe6 |
||||
bdfe8 |
||||
bdfe9 |
||||
bdff1 |
||||
bdFF10 |
||||
bdFF11 |
||||
bdFF12 |
||||
bdFF13 |
||||
bdFF14 |
||||
bdFF15 |
||||
bdff2 |
||||
bdff3 |
||||
bdff4 |
||||
bdff5 |
||||
bdff6 |
||||
bdff9 |
||||
bdFrFA |
||||
bdGB1 |
||||
bdGB2 |
||||
bdGB3 |
||||
bdGB4 |
||||
bdGB5 |
||||
bdGT |
||||
bdHH |
||||
bdHOBI |
||||
bdHOBII |
||||
bdHoBIII |
||||
bdHoBIV |
||||
bdJ1 |
||||
bdKB |
||||
bdkholmS |
||||
bdkholmW |
||||
bdkholmWO |
||||
bdKOTH |
||||
bdKR |
||||
bdKreta |
||||
bdLCP-Caslo |
||||
bdLCP-Crossroads |
||||
bdLCP-GYMO |
||||
bdLCP-JAVA09 |
||||
bdLCP-JAVA10 |
||||
bdLCP-JAVA11 |
||||
bdLCP-JAVA12 |
||||
bdLCP-JAVA13 |
||||
bdLCP-JAVA14 |
||||
bdLCPDtO |
||||
bdLFT1 |
||||
bdLFT2 |
||||
bdLG |
||||
bdNG |
||||
bdNUL |
||||
bdNULV |
||||
bdOTO |
||||
bdoto2 |
||||
bdOzB |
||||
bdPB |
||||
bdPBR |
||||
bdPdH |
||||
bdPHD |
||||
bdRaM |
||||
bdRB |
||||
bdRBv2 |
||||
bdRees |
||||
bdRileys |
||||
bdRR |
||||
bdSC |
||||
bdSCW0 |
||||
bdSCW1 |
||||
bdSCW2 |
||||
bdSenno |
||||
bdSG |
||||
bdSH |
||||
bdST |
||||
bdStN |
||||
bdTauroggen |
||||
bdTCRS |
||||
bdThinLine |
||||
bdTO |
||||
bdTR1 |
||||
bdTR2 |
||||
bdTR3 |
||||
bdTR4 |
||||
bdTR5 |
||||
bdTR6 |
||||
bdTR7 |
||||
bdTR8 |
||||
bdTRBH |
||||
bdVotG |
||||
bdWittLastBattle |
||||
KB |
||||
RR |
||||
SH |
||||
TRBH |
Binary file not shown.
@ -0,0 +1,51 @@ |
||||
package vassal_shim ; |
||||
|
||||
import java.util.ArrayList ; |
||||
|
||||
import VASSAL.counters.GamePiece ; |
||||
|
||||
// --------------------------------------------------------------------
|
||||
|
||||
public class GamePieceLabelFields |
||||
{ |
||||
// Holds the individual fields in a GamePiece label.
|
||||
// A GamePiece's state is a string, consisting of a number of separated fields.
|
||||
// We parse the string into its constituent parts, so that we can make changes
|
||||
// to some fields (i.e. the label content), and re-constitute the state string.
|
||||
|
||||
// These fields contain label #1 and #2.
|
||||
public static final int FIELD_INDEX_LABEL1 = 3 ; |
||||
public static final int FIELD_INDEX_LABEL2 = 4 ; |
||||
|
||||
private GamePiece gamePiece ; |
||||
private ArrayList<String> fields ; |
||||
private ArrayList<String> separators ; |
||||
private int fieldIndex ; |
||||
|
||||
public GamePiece gamePiece() { return gamePiece ; } |
||||
public String getLabelContent() { return getLabelContent( this.fieldIndex ) ; } |
||||
public String getLabelContent( int fieldIndex ) { return fieldIndex < fields.size() ? fields.get(fieldIndex) : null ; } |
||||
public void setFieldIndex( int fieldIndex ) { this.fieldIndex = fieldIndex ; } |
||||
|
||||
public GamePieceLabelFields( GamePiece gamePiece, ArrayList<String> separators, ArrayList<String> fields, int fieldIndex ) |
||||
{ |
||||
this.gamePiece = gamePiece ; |
||||
this.separators = separators ; |
||||
this.fields = fields ; |
||||
this.fieldIndex = fieldIndex ; |
||||
} |
||||
|
||||
public String getNewGamePieceState( String newField ) |
||||
{ |
||||
// get the GamePiece's state wih the new field
|
||||
fields.set( fieldIndex, newField ) ; |
||||
StringBuilder buf = new StringBuilder() ; |
||||
for ( int i=0 ; i < fields.size() ; ++i ) { |
||||
buf.append( fields.get( i ) ) ; |
||||
if ( i < separators.size() ) |
||||
buf.append( separators.get( i ) ) ; |
||||
} |
||||
return buf.toString() ; |
||||
} |
||||
} |
||||
|
@ -0,0 +1,107 @@ |
||||
package vassal_shim ; |
||||
|
||||
import java.awt.Point ; |
||||
|
||||
import org.slf4j.Logger ; |
||||
import org.slf4j.LoggerFactory ; |
||||
|
||||
// --------------------------------------------------------------------
|
||||
|
||||
public class LabelArea |
||||
{ |
||||
// Represents a rectangular area on the map in which we will put labels.
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger( LabelArea.class ) ; |
||||
|
||||
private String labelAreaName ; |
||||
private Point topLeft ; |
||||
private int areaWidth, areaHeight ; |
||||
private int xMargin, yMargin ; |
||||
private Point currPos ; |
||||
private int currRowHeight ; |
||||
|
||||
public String getName() { return labelAreaName ; } |
||||
|
||||
public LabelArea( String name, Point topLeft, int width, int height, int xMargin, int yMargin ) |
||||
{ |
||||
logger.info( "Creating LabelArea '{}': topLeft=[{},{}] ; size={}x{} ; xMargin={} ; yMargin={}", |
||||
name, topLeft.x, topLeft.y, width, height, xMargin, yMargin |
||||
) ; |
||||
this.labelAreaName = name ; |
||||
this.topLeft = topLeft ; |
||||
this.areaWidth = width ; |
||||
this.areaHeight = height ; |
||||
this.xMargin = xMargin ; |
||||
this.yMargin = yMargin ; |
||||
this.currPos = new Point( 0, 0 ) ; // nb: relative to topLeft
|
||||
this.currRowHeight = 0 ; |
||||
} |
||||
|
||||
public Point getNextPosition( String snippet_id, int labelWidth, int labelHeight ) |
||||
{ |
||||
// NOTE: When trying to position the label, we allow overflow of up to 40% of the label's width,
|
||||
// since that will still put the label's centre (which is the click target) in a clear part of the map.
|
||||
|
||||
// check if the label will fit in the next available position
|
||||
logger.debug( "Getting next label position ({}): label={}x{}, currPos=[{},{}]", |
||||
labelAreaName, labelWidth, labelHeight, currPos.x, currPos.y |
||||
) ; |
||||
int overflow = (currPos.x + labelWidth) - areaWidth ; |
||||
logger.debug( "- h.overflow = {}", overflow ) ; |
||||
if ( overflow < 0.4 * labelWidth ) { |
||||
// we have enough horizontal space to place the label, check vertically
|
||||
overflow = (currPos.y + labelHeight) - areaHeight ; |
||||
logger.debug( "- can use current row, v.overflow={}", overflow ) ; |
||||
if ( overflow < 0.4 * labelHeight ) { |
||||
// we have enough vertical space as well, put the label in the next available position
|
||||
logger.debug( "- can use next available position: [{},{}]", currPos.x, currPos.y ) ; |
||||
Point assignedPos = new Point( topLeft.x+currPos.x, topLeft.y+currPos.y ) ; |
||||
currPos.x += labelWidth + xMargin ; |
||||
currRowHeight = Math.max( currRowHeight, labelHeight ) ; |
||||
return assignedPos ; |
||||
} else { |
||||
// the LabelArea is full - notify the caller
|
||||
logger.debug( "- LabelArea is full!" ) ; |
||||
return null ; |
||||
} |
||||
} else { |
||||
// there isn't enough horizontal space to place the label, start a new row
|
||||
doStartNewRow() ; |
||||
logger.debug( "- starting a new row: y={}",currPos.y ) ; |
||||
// put the label at the start of the new row
|
||||
if ( labelWidth > areaWidth ) { |
||||
// the label is wider than the available width- centre it
|
||||
currPos.x = (areaWidth - labelWidth) / 2 ; |
||||
} |
||||
overflow = (currPos.y + labelHeight) - areaHeight ; |
||||
logger.debug( "- v.overflow = {}", overflow ) ; |
||||
if ( overflow >= 0.4 * labelHeight ) { |
||||
// the LabelArea is full - notify the caller
|
||||
logger.debug( "- LabelArea is full!" ) ; |
||||
return null ; |
||||
} |
||||
logger.debug( "- assigning position: [{},{}]", currPos.x, currPos.y ) ; |
||||
Point assignedPos = new Point( topLeft.x+currPos.x, topLeft.y+currPos.y ) ; |
||||
currPos.x += labelWidth + xMargin ; |
||||
currRowHeight = Math.max( currRowHeight, labelHeight ) ; |
||||
return assignedPos ; |
||||
} |
||||
} |
||||
|
||||
public void startNewRow( String snippetId ) |
||||
{ |
||||
// start a new row
|
||||
doStartNewRow() ; |
||||
logger.debug( "Started a new row for '{}': y={}", snippetId, currPos.y ) ; |
||||
} |
||||
|
||||
private void doStartNewRow() |
||||
{ |
||||
// start a new row
|
||||
if ( currPos.x == 0 ) |
||||
return ; |
||||
currPos.x = 0 ; |
||||
currPos.y += currRowHeight + yMargin ; |
||||
currRowHeight = 0 ; |
||||
} |
||||
} |
@ -0,0 +1,62 @@ |
||||
package vassal_shim ; |
||||
|
||||
import vassal_shim.VassalShim ; |
||||
|
||||
// --------------------------------------------------------------------
|
||||
|
||||
public class Main |
||||
{ |
||||
public static void main( String[] args ) |
||||
{ |
||||
// parse the command line arguments
|
||||
if ( args.length == 0 ) { |
||||
printHelp() ; |
||||
System.exit( 0 ) ; |
||||
} |
||||
|
||||
// execute the specified command
|
||||
try { |
||||
String cmd = args[0].toLowerCase() ; |
||||
if ( cmd.equals( "dump" ) ) { |
||||
checkArgs( args, 3, "the VASL .vmod file and scenario file" ) ; |
||||
VassalShim shim = new VassalShim( args[1], null ) ; |
||||
shim.dumpScenario( args[2] ) ; |
||||
System.exit( 0 ) ; |
||||
} |
||||
else if ( cmd.equals( "update" ) ) { |
||||
checkArgs( args, 7, "the VASL .vmod file, boards directory, scenario file, snippets file and output/report files" ) ; |
||||
VassalShim shim = new VassalShim( args[1], args[2] ) ; |
||||
shim.updateScenario( args[3], args[4], args[5], args[6] ) ; |
||||
System.exit( 0 ) ; |
||||
} |
||||
else { |
||||
System.out.println( "Unknown command: " + cmd ) ; |
||||
System.exit( 1 ) ; |
||||
} |
||||
} catch( Exception ex ) { |
||||
System.out.println( "ERROR: " + ex ) ; |
||||
ex.printStackTrace( System.out ) ; |
||||
System.exit( -1 ) ; |
||||
} |
||||
} |
||||
|
||||
private static void checkArgs( String[]args, int expected, String hint ) |
||||
{ |
||||
// check the number of arguments
|
||||
if ( args.length != expected ) { |
||||
System.out.println( "Incorrect number of arguments, please specify " + hint + "." ) ; |
||||
System.exit( 2 ) ; |
||||
} |
||||
} |
||||
|
||||
private static void printHelp() |
||||
{ |
||||
// show program usage
|
||||
System.out.println( Main.class.getName() + " {command} {options}" ) ; |
||||
System.out.println( " Provide access to VASSAL functionality." ) ; |
||||
System.out.println() ; |
||||
System.out.println( "Available commands:" ) ; |
||||
System.out.println( " dump: Dump a .vsav file." ) ; |
||||
System.out.println( " update: Update the labels in a .vsav file." ) ; |
||||
} |
||||
} |
@ -0,0 +1,17 @@ |
||||
package vassal_shim ; |
||||
|
||||
import javax.swing.JFrame ; |
||||
import javax.swing.JMenuBar ; |
||||
|
||||
import VASSAL.tools.menu.MenuManager ; |
||||
import VASSAL.tools.menu.MenuBarProxy ; |
||||
|
||||
// --------------------------------------------------------------------
|
||||
|
||||
public class ModuleManagerMenuManager extends MenuManager |
||||
{ |
||||
private final MenuBarProxy menuBar = new MenuBarProxy() ; |
||||
|
||||
public JMenuBar getMenuBarFor( JFrame fc ) { return null ; } |
||||
public MenuBarProxy getMenuBarProxyFor( JFrame fc ) { return menuBar ; } |
||||
} |
@ -0,0 +1,20 @@ |
||||
package vassal_shim ; |
||||
|
||||
import java.awt.Point ; |
||||
|
||||
// --------------------------------------------------------------------
|
||||
|
||||
public class ReportNode |
||||
{ |
||||
// POD container that holds information about what was done.
|
||||
|
||||
String snippetId ; |
||||
Point labelPos ; |
||||
|
||||
public ReportNode( String snippetId, Point labelPos ) |
||||
{ |
||||
// initialize the ReportNode
|
||||
this.snippetId = snippetId ; |
||||
this.labelPos = labelPos ; |
||||
} |
||||
} |
@ -0,0 +1,57 @@ |
||||
package vassal_shim ; |
||||
|
||||
import java.util.Properties ; |
||||
import java.util.ArrayList ; |
||||
import org.w3c.dom.NodeList ; |
||||
import org.w3c.dom.Node ; |
||||
import org.w3c.dom.Element ; |
||||
import javax.xml.xpath.XPathFactory ; |
||||
import javax.xml.xpath.XPath ; |
||||
import javax.xml.xpath.XPathExpression ; |
||||
import javax.xml.xpath.XPathExpressionException ; |
||||
import javax.xml.xpath.XPathConstants ; |
||||
|
||||
import vassal_shim.Utils ; |
||||
|
||||
// --------------------------------------------------------------------
|
||||
|
||||
public class Snippet |
||||
{ |
||||
// POD container that holds the snippet information sent to us from the web server.
|
||||
|
||||
public String snippetId ; |
||||
public String content ; |
||||
public ArrayList<String> rawContent ; |
||||
public int width, height ; |
||||
public boolean autoCreate ; |
||||
public String labelArea ; |
||||
|
||||
public Snippet( Element elem, Properties config ) throws XPathExpressionException |
||||
{ |
||||
// initialize
|
||||
XPathFactory xpathFactory = XPathFactory.newInstance() ; |
||||
|
||||
// initialize the Snippet
|
||||
this.snippetId = elem.getAttribute( "id" ) ; |
||||
this.content = Utils.getNodeTextContent( elem ) ; |
||||
String snippetWidth = elem.getAttribute( "width" ) ; |
||||
this.width = Integer.parseInt( |
||||
snippetWidth != "" ? snippetWidth : config.getProperty("AUTOCREATE_LABEL_DEFAULT_WIDTH","300") |
||||
) ; |
||||
String snippetHeight = elem.getAttribute( "height" ) ; |
||||
this.height = Integer.parseInt( |
||||
snippetHeight != "" ? snippetHeight : config.getProperty("AUTOCREATE_LABEL_DEFAULT_HEIGHT","300") |
||||
) ; |
||||
this.autoCreate = elem.getAttribute( "autoCreate" ).equals( "true" ) ; |
||||
this.labelArea = elem.getAttribute( "labelArea" ) ; |
||||
|
||||
// initialize the Snippet
|
||||
this.rawContent = new ArrayList<String>() ; |
||||
XPathExpression expr = xpathFactory.newXPath().compile( "rawContent/phrase/text()" ) ; |
||||
NodeList nodes = (NodeList) expr.evaluate( elem, XPathConstants.NODESET ) ; |
||||
for ( int i=0 ; i < nodes.getLength() ; ++i ) { |
||||
Node node = nodes.item( i ) ; |
||||
this.rawContent.add( nodes.item(i).getTextContent() ) ; |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,45 @@ |
||||
package vassal_shim ; |
||||
|
||||
import org.w3c.dom.NodeList ; |
||||
import org.w3c.dom.Node ; |
||||
|
||||
// --------------------------------------------------------------------
|
||||
|
||||
public class Utils |
||||
{ |
||||
public static String getNodeTextContent( Node node ) |
||||
{ |
||||
// get the text content for an XML node (just itself, no descendants)
|
||||
StringBuilder buf = new StringBuilder() ; |
||||
NodeList childNodes = node.getChildNodes() ; |
||||
for ( int i=0 ; i < childNodes.getLength() ; ++i ) { |
||||
Node childNode = childNodes.item( i ) ; |
||||
if ( childNode.getNodeName().equals( "#text" ) ) |
||||
buf.append( childNode.getTextContent() ) ; |
||||
} |
||||
return buf.toString() ; |
||||
} |
||||
|
||||
public static boolean startsWith( String val, String target ) |
||||
{ |
||||
// check if a string starts with a target substring
|
||||
if ( val.length() < target.length() ) |
||||
return false ; |
||||
return val.substring( 0, target.length() ).equals( target ) ; |
||||
} |
||||
|
||||
public static String printableString( String val ) |
||||
{ |
||||
// encode non-ASCII characters
|
||||
if ( val == null ) |
||||
return "<null>" ; |
||||
StringBuilder buf = new StringBuilder() ; |
||||
for ( char ch: val.toCharArray() ) { |
||||
if ( (int)ch >= 32 && (int)ch <= 127 ) |
||||
buf.append( ch ) ; |
||||
else |
||||
buf.append( String.format( "<%02x>", (int)ch ) ) ; |
||||
} |
||||
return buf.toString() ; |
||||
} |
||||
} |
@ -0,0 +1,884 @@ |
||||
package vassal_shim ; |
||||
|
||||
import java.io.File ; |
||||
import java.io.FileInputStream ; |
||||
import java.io.InputStream ; |
||||
import java.io.InputStreamReader ; |
||||
import java.io.FileOutputStream ; |
||||
import java.io.OutputStream ; |
||||
import java.io.BufferedReader ; |
||||
import java.io.IOException ; |
||||
import java.io.FileNotFoundException ; |
||||
import java.net.URISyntaxException ; |
||||
import java.util.Collections ; |
||||
import java.util.Arrays ; |
||||
import java.util.List ; |
||||
import java.util.ArrayList ; |
||||
import java.util.Map ; |
||||
import java.util.HashMap ; |
||||
import java.util.Set ; |
||||
import java.util.HashSet ; |
||||
import java.util.Iterator ; |
||||
import java.util.Comparator ; |
||||
import java.util.Properties ; |
||||
import java.util.regex.Pattern ; |
||||
import java.util.regex.Matcher ; |
||||
import java.awt.Point ; |
||||
import java.awt.Dimension ; |
||||
|
||||
import javax.xml.parsers.DocumentBuilderFactory ; |
||||
import javax.xml.parsers.DocumentBuilder ; |
||||
import javax.xml.parsers.ParserConfigurationException ; |
||||
import javax.xml.transform.Transformer ; |
||||
import javax.xml.transform.TransformerException ; |
||||
import javax.xml.transform.TransformerConfigurationException ; |
||||
import javax.xml.transform.TransformerFactory ; |
||||
import javax.xml.transform.OutputKeys ; |
||||
import javax.xml.transform.dom.DOMSource ; |
||||
import javax.xml.transform.stream.StreamResult ; |
||||
import javax.xml.xpath.XPathExpressionException ; |
||||
import org.w3c.dom.Document ; |
||||
import org.w3c.dom.NodeList ; |
||||
import org.w3c.dom.Node ; |
||||
import org.w3c.dom.Element ; |
||||
import org.xml.sax.SAXException ; |
||||
import org.slf4j.Logger ; |
||||
import org.slf4j.LoggerFactory ; |
||||
|
||||
import VASSAL.build.GameModule ; |
||||
import VASSAL.build.GpIdChecker ; |
||||
import VASSAL.build.module.GameState ; |
||||
import VASSAL.build.module.GameComponent ; |
||||
import VASSAL.build.module.ModuleExtension ; |
||||
import VASSAL.build.module.ObscurableOptions ; |
||||
import VASSAL.build.module.metadata.SaveMetaData ; |
||||
import VASSAL.build.widget.PieceSlot ; |
||||
import VASSAL.launch.BasicModule ; |
||||
import VASSAL.command.Command ; |
||||
import VASSAL.command.AddPiece ; |
||||
import VASSAL.command.RemovePiece ; |
||||
import VASSAL.command.ConditionalCommand ; |
||||
import VASSAL.command.AlertCommand ; |
||||
import VASSAL.build.module.map.boardPicker.Board ; |
||||
import VASSAL.counters.GamePiece ; |
||||
import VASSAL.counters.DynamicProperty ; |
||||
import VASSAL.counters.PieceCloner ; |
||||
import VASSAL.preferences.Prefs ; |
||||
import VASSAL.tools.DataArchive ; |
||||
import VASSAL.tools.DialogUtils ; |
||||
import VASSAL.tools.io.FileArchive ; |
||||
import VASSAL.tools.io.IOUtils ; |
||||
import VASSAL.tools.io.FastByteArrayOutputStream ; |
||||
import VASSAL.tools.io.ObfuscatingOutputStream ; |
||||
import VASSAL.tools.io.ZipArchive ; |
||||
import VASSAL.i18n.Resources ; |
||||
|
||||
import vassal_shim.Snippet ; |
||||
import vassal_shim.GamePieceLabelFields ; |
||||
import vassal_shim.LabelArea ; |
||||
import vassal_shim.ReportNode ; |
||||
import vassal_shim.ModuleManagerMenuManager ; |
||||
import vassal_shim.Utils ; |
||||
|
||||
// --------------------------------------------------------------------
|
||||
|
||||
public class VassalShim |
||||
{ |
||||
private static final Logger logger = LoggerFactory.getLogger( VassalShim.class ) ; |
||||
|
||||
private String baseDir ; |
||||
private Properties config ; |
||||
private String vmodFilename ; |
||||
private String boardsDir ; |
||||
|
||||
public VassalShim( String vmodFilename, String boardsDir ) throws IOException |
||||
{ |
||||
// initialize
|
||||
this.vmodFilename = vmodFilename ; |
||||
this.boardsDir = boardsDir ; |
||||
|
||||
// figure out where we live
|
||||
baseDir = null ; |
||||
try { |
||||
String jarFilename = this.getClass().getProtectionDomain().getCodeSource().getLocation().toURI().getPath() ; |
||||
logger.debug( "Loaded from JAR: {}", jarFilename ) ; |
||||
baseDir = new File( jarFilename ).getParent() ; |
||||
logger.debug( "Base directory: {}", baseDir ) ; |
||||
} catch( URISyntaxException ex ) { |
||||
logger.error( "Can't locate JAR file:", ex ) ; |
||||
} |
||||
|
||||
// load any config settings
|
||||
config = new Properties() ; |
||||
if ( baseDir != null ) { |
||||
File configFile = new File( baseDir + File.separator + "vassal-shim.properties" ) ; |
||||
if ( configFile.isFile() ) { |
||||
logger.info( "Loading properties: {}", configFile.getAbsolutePath() ) ; |
||||
config.load( new FileInputStream( configFile ) ) ; |
||||
for ( String key: config.stringPropertyNames() ) |
||||
logger.debug( "- {} = {}", key, config.getProperty(key) ) ; |
||||
} |
||||
} |
||||
|
||||
// FUDGE! Need this to be able to load the VASL module :-/
|
||||
logger.debug( "Creating the menu manager." ) ; |
||||
new ModuleManagerMenuManager() ; |
||||
|
||||
// initialize VASL
|
||||
logger.info( "Loading VASL module: {}", vmodFilename ) ; |
||||
if ( ! ((new File(vmodFilename)).isFile() ) ) |
||||
throw new IllegalArgumentException( "Can't find VASL module: " + vmodFilename ) ; |
||||
DataArchive dataArchive = new DataArchive( vmodFilename ) ; |
||||
logger.debug( "- Initializing module." ) ; |
||||
BasicModule basicModule = new BasicModule( dataArchive ) ; |
||||
logger.debug( "- Installing module." ) ; |
||||
GameModule.init( basicModule ) ; |
||||
logger.debug( "- Loaded OK." ) ; |
||||
} |
||||
|
||||
public void dumpScenario( String scenarioFilename ) throws IOException |
||||
{ |
||||
// load the scenario and dump its commands
|
||||
Command cmd = loadScenario( scenarioFilename ) ; |
||||
dumpCommand( cmd, "" ) ; |
||||
} |
||||
|
||||
public void updateScenario( String scenarioFilename, String snippetsFilename, String saveFilename, String reportFilename ) |
||||
throws IOException, ParserConfigurationException, SAXException, XPathExpressionException, TransformerException |
||||
{ |
||||
// load the snippets supplied to us by the web server
|
||||
Map<String,Snippet> snippets = parseSnippets( snippetsFilename ) ; |
||||
|
||||
// NOTE: While we can get away with just disabling warnings about missing boards when dumping scenarios,
|
||||
// they need to be present when we update a scenario, otherwise they get removed from the scenario :-/
|
||||
logger.info( "Configuring boards directory: {}", boardsDir ) ; |
||||
Prefs prefs = GameModule.getGameModule().getPrefs() ; |
||||
String BOARD_DIR = "boardURL" ; |
||||
prefs.setValue( BOARD_DIR, new File(boardsDir) ) ; |
||||
|
||||
// load the scenario
|
||||
Command cmd = loadScenario( scenarioFilename ) ; |
||||
// NOTE: The call to execute() is what's causing the VASSAL UI to appear on-screen. If we take it out,
|
||||
// label creation still works, but any boards and existing labels are not detected, presumably because
|
||||
// their Command's need to be executed to take effect.
|
||||
cmd.execute() ; |
||||
|
||||
// extract the labels from the scenario
|
||||
Map<String,GamePieceLabelFields> ourLabels = new HashMap<String,GamePieceLabelFields>() ; |
||||
ArrayList<GamePieceLabelFields> otherLabels = new ArrayList<GamePieceLabelFields>() ; |
||||
logger.info( "Searching the VASL scenario for labels..." ) ; |
||||
extractLabels( cmd, ourLabels, otherLabels ) ; |
||||
|
||||
// update the labels from the snippets
|
||||
Map< String, ArrayList<ReportNode> > labelReport = processSnippets( ourLabels, otherLabels, snippets ) ; |
||||
|
||||
// save the scenario
|
||||
saveScenario( saveFilename ) ; |
||||
|
||||
// generate the report
|
||||
generateLabelReport( labelReport, reportFilename ) ; |
||||
|
||||
// NOTE: The test suite always dumps the scenario after updating it, so we could save a lot of time
|
||||
// by dumping it here, thus avoiding the need to run this shim again to do the dump (and spinning up
|
||||
// a JVM, initializing VASSAL/VASL, etc.) but it's probably worth doing things the slow way, to avoid
|
||||
// any possible problems caused by reusing the current session (e.g. there might be some saved state somewhere).
|
||||
} |
||||
|
||||
private Map<String,Snippet> parseSnippets( String snippetsFilename ) throws IOException, ParserConfigurationException, SAXException, XPathExpressionException |
||||
{ |
||||
logger.info( "Loading snippets: {}", snippetsFilename ) ; |
||||
Map<String,Snippet> snippets = new HashMap<String,Snippet>() ; |
||||
|
||||
// load the snippets
|
||||
DocumentBuilderFactory docBuilderFactory = DocumentBuilderFactory.newInstance() ; |
||||
DocumentBuilder docBuilder = docBuilderFactory.newDocumentBuilder() ; |
||||
Document doc = docBuilder.parse( new File( snippetsFilename ) ) ; |
||||
doc.getDocumentElement().normalize() ; |
||||
NodeList nodes = doc.getElementsByTagName( "snippet" ) ; |
||||
for ( int i=0 ; i < nodes.getLength() ; ++i ) { |
||||
Node node = nodes.item( i ) ; |
||||
if ( node.getNodeType() != Node.ELEMENT_NODE ) |
||||
continue ; |
||||
Snippet snippet = new Snippet( (Element)node, config ) ; |
||||
logger.debug( "- Added snippet '{}' [{}x{}] (labelArea={}) (autoCreate={}):\n{}", |
||||
snippet.snippetId, |
||||
snippet.width, snippet.height, |
||||
snippet.labelArea, |
||||
snippet.autoCreate, snippet.content |
||||
) ; |
||||
snippets.put( snippet.snippetId, snippet ) ; |
||||
} |
||||
|
||||
return snippets ; |
||||
} |
||||
|
||||
private void extractLabels( Command cmd, Map<String,GamePieceLabelFields> ourLabels, ArrayList<GamePieceLabelFields> otherLabels ) |
||||
{ |
||||
// check if this command is a label we're interested in
|
||||
if ( cmd instanceof AddPiece ) { |
||||
AddPiece addPieceCmd = (AddPiece) cmd ; |
||||
if ( addPieceCmd.getTarget() instanceof DynamicProperty ) { |
||||
GamePiece target = addPieceCmd.getTarget() ; |
||||
// NOTE: We can't check for target.getName() == "User-Labeled", it seems to get changed to the first label :shrug:
|
||||
|
||||
// yup - parse the label content
|
||||
ArrayList<String> separators = new ArrayList<String>() ; |
||||
ArrayList<String> fields = new ArrayList<String>() ; |
||||
parseGamePieceState( target.getState(), separators, fields ) ; |
||||
|
||||
// check if the label is one of ours
|
||||
String snippetId = isVaslTemplatesLabel( fields, GamePieceLabelFields.FIELD_INDEX_LABEL1 ) ; |
||||
if ( snippetId != null ) { |
||||
logger.debug( "- Found label (1): {}", snippetId ) ; |
||||
ourLabels.put( snippetId, |
||||
new GamePieceLabelFields( target, separators, fields, GamePieceLabelFields.FIELD_INDEX_LABEL1 ) |
||||
) ; |
||||
} |
||||
else { |
||||
snippetId = isVaslTemplatesLabel( fields, GamePieceLabelFields.FIELD_INDEX_LABEL2 ) ; |
||||
if ( snippetId != null ) { |
||||
logger.debug( "- Found label (2): {}", snippetId ) ; |
||||
ourLabels.put( snippetId, |
||||
new GamePieceLabelFields( target, separators, fields, GamePieceLabelFields.FIELD_INDEX_LABEL2 ) |
||||
) ; |
||||
} else { |
||||
otherLabels.add( |
||||
new GamePieceLabelFields( target, separators, fields, -1 ) |
||||
) ; |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
// extract labels in sub-commands
|
||||
for ( Command c: cmd.getSubCommands() ) |
||||
extractLabels( c, ourLabels, otherLabels ) ; |
||||
} |
||||
|
||||
private String isVaslTemplatesLabel( ArrayList<String> fields, int fieldIndex ) |
||||
{ |
||||
// check if a label is one of ours
|
||||
if ( fieldIndex >= fields.size() ) |
||||
return null ; |
||||
Matcher matcher = Pattern.compile( "<!-- vasl-templates:id (.+?) " ).matcher( |
||||
fields.get( fieldIndex ) |
||||
) ; |
||||
if ( ! matcher.find() ) |
||||
return null ; |
||||
return matcher.group( 1 ) ; |
||||
} |
||||
|
||||
private Map< String, ArrayList<ReportNode> > |
||||
processSnippets( Map<String,GamePieceLabelFields> ourLabels, ArrayList<GamePieceLabelFields> otherLabels, Map<String,Snippet> snippets ) |
||||
{ |
||||
// initialize
|
||||
Map< String, ArrayList<ReportNode> > labelReport = new HashMap<String,ArrayList<ReportNode>>() ; |
||||
for ( String key: new String[]{"created","updated","deleted","unchanged"} ) |
||||
labelReport.put( key, new ArrayList<ReportNode>() ) ; |
||||
|
||||
// process each snippet
|
||||
logger.info( "Processing snippets..." ) ; |
||||
Iterator< Map.Entry<String,Snippet> > iter = snippets.entrySet().iterator() ; |
||||
while( iter.hasNext() ) { |
||||
Map.Entry<String,Snippet> entry = iter.next() ; |
||||
String snippetId = entry.getKey() ; |
||||
Snippet snippet = entry.getValue() ; |
||||
if ( Utils.startsWith( snippetId, "extras/" ) ) { |
||||
logger.info( "- Skipping extras snippet: " + snippetId ) ; |
||||
continue ; |
||||
} |
||||
logger.debug( "- Processing snippet: {}", snippetId ) ; |
||||
// check if we have a label with a matching snippet ID
|
||||
GamePieceLabelFields labelFields = ourLabels.get( snippetId ) ; |
||||
if ( labelFields != null ) { |
||||
logger.debug( " - Found matching label." ) ; |
||||
ourLabels.remove( snippetId ) ; |
||||
} else { |
||||
// nope - check if there is a legacy label that corresponds to this snippet
|
||||
labelFields = findLegacyLabel( otherLabels, snippet ) ; |
||||
if ( labelFields != null ) |
||||
logger.debug( " - Found matching legacy label." ) ; |
||||
else { |
||||
// nope - skip this snippet (we will create a new label for it later)
|
||||
logger.debug( " - Couldn't find matching label." ) ; |
||||
continue ; |
||||
} |
||||
} |
||||
// we've match the snippet to a label, update the label content
|
||||
String currState = labelFields.gamePiece().getState() ; |
||||
String snippetContent = snippet.content.replace( "\n", " " ) ; |
||||
String newState = labelFields.getNewGamePieceState( snippetContent ) ; |
||||
if ( currState.equals( newState ) ) { |
||||
logger.info( "- Skipping label (unchanged): {}", snippetId ) ; |
||||
labelReport.get( "unchanged" ).add( new ReportNode( snippetId, null ) ) ; |
||||
} else { |
||||
logger.info( "- Updating label: {}", snippetId ) ; |
||||
logger.debug( " - curr state: " + Utils.printableString(currState) ) ; |
||||
logger.debug( " - new state: " + Utils.printableString(newState) ) ; |
||||
labelFields.gamePiece().setState( newState ) ; |
||||
labelReport.get( "updated" ).add( new ReportNode( snippetId, null ) ) ; |
||||
} |
||||
iter.remove() ; |
||||
} |
||||
|
||||
// delete excess labels
|
||||
// NOTE: This will only affect labels that have a snippet ID i.e. legacy labels will be left in place.
|
||||
for ( String snippetId: ourLabels.keySet() ) { |
||||
if ( Utils.startsWith( snippetId, "extras/" ) ) |
||||
continue ; |
||||
logger.info( "- Deleting label: {}", snippetId ) ; |
||||
GamePieceLabelFields labelFields = ourLabels.get( snippetId ) ; |
||||
RemovePiece cmd = new RemovePiece( labelFields.gamePiece() ) ; |
||||
cmd.execute() ; |
||||
labelReport.get( "deleted" ).add( new ReportNode( snippetId, null ) ) ; |
||||
} |
||||
|
||||
// We now want to create new labels for any snippets left that haven't already been processed.
|
||||
//
|
||||
// We divide the map into several areas:
|
||||
// +------------------------------------------+
|
||||
// | GENERAL |
|
||||
// +------------+----------------+------------+
|
||||
// | | | |
|
||||
// | PLAYER 1 | board(s) | PLAYER 2 |
|
||||
// | | | |
|
||||
// | |----------------| +
|
||||
// | | OVERFLOW | |
|
||||
// +------------------------------------------+
|
||||
// Non-player specific labels (e.g. SCENARIO and SSR) go into GENERAL, player-specific labels
|
||||
// go into their respective areas, and everything else left over that didn't fit into their
|
||||
// normal area goes into OVERFLOW.
|
||||
//
|
||||
// The exception to this is if the scenario contains no boards, in which case we just create
|
||||
// a single GENERAL area that spans the entire available space.
|
||||
//
|
||||
// NOTE: We don't consider any labels that might already be present in the scenario. While we could
|
||||
// handle this, it would slow down an already slow process i.e. the web server would have to dump
|
||||
// the scenario, extract any existing labels, calculate their size, then pass that information
|
||||
// back to us, so that we can take them into account when placing new labels (which would also
|
||||
// then become much more complicated). It's just not worth it for something that will rarely happen.
|
||||
|
||||
// locate the PieceSlot we will use to create labels
|
||||
String labelGpid = config.getProperty( "LABEL_GPID", "6295" ) ; |
||||
logger.debug( "- Locating PieceSlot: gpid={}", labelGpid ) ; |
||||
PieceSlot labelPieceSlot = null ; |
||||
GpIdChecker gpidChecker = new GpIdChecker() ; |
||||
for ( PieceSlot pieceSlot : GameModule.getGameModule().getAllDescendantComponentsOf( PieceSlot.class ) ) { |
||||
if ( pieceSlot.getGpId().equals( labelGpid ) ) { |
||||
labelPieceSlot = pieceSlot ; |
||||
break ; |
||||
} |
||||
} |
||||
if ( labelPieceSlot == null ) |
||||
throw new IllegalArgumentException( "Can't find PieceSlot: gpid=" + labelGpid ) ; |
||||
|
||||
// initialize our LabelArea's
|
||||
int xMargin = Integer.parseInt( config.getProperty( "AUTOCREATE_LABEL_XMARGIN", "20" ) ) ; |
||||
int yMargin = Integer.parseInt( config.getProperty( "AUTOCREATE_LABEL_YMARGIN", "20" ) ) ; |
||||
Map< String, LabelArea > labelAreas = new HashMap<String,LabelArea>() ; |
||||
VASSAL.build.module.Map map ; |
||||
List<VASSAL.build.module.Map> maps = VASSAL.build.module.Map.getMapList() ; |
||||
if ( maps.size() > 1 ) |
||||
logger.warn( "WARNING: Found multiple maps - using the first one." ) ; |
||||
map = maps.get( 0 ) ; |
||||
if ( map.getBoardCount() == 0 ) |
||||
// the scenario doesn't contain any boards - we create a single GENERAL area that spans
|
||||
// the entire map (we assume a single board width, and unlimited height)
|
||||
labelAreas.put( "general", |
||||
new LabelArea( "general", new Point(xMargin,yMargin), 2500, 99999, xMargin, yMargin ) |
||||
) ; |
||||
else { |
||||
// get the total amount of space available
|
||||
Dimension mapSize = map.mapSize() ; |
||||
int mapWidth = mapSize.width ; |
||||
int mapHeight = mapSize.height ; |
||||
// get the amount of empty space around the boards
|
||||
Dimension edgeBuffer = map.getEdgeBuffer() ; |
||||
int borderWidth = edgeBuffer.width ; |
||||
int borderHeight = edgeBuffer.height ; |
||||
labelAreas.put( "general", |
||||
new LabelArea( "general", |
||||
new Point( xMargin, yMargin ), |
||||
mapWidth-2*xMargin, borderHeight-2*yMargin, |
||||
xMargin, yMargin |
||||
) |
||||
) ; |
||||
labelAreas.put( "player1", |
||||
new LabelArea( "player1", |
||||
new Point( xMargin, borderHeight ), |
||||
borderWidth-2*xMargin, mapHeight-borderHeight, |
||||
xMargin, yMargin |
||||
) |
||||
) ; |
||||
labelAreas.put( "player2", |
||||
new LabelArea( "player2", |
||||
new Point( mapWidth-borderWidth+xMargin, borderHeight ), |
||||
borderWidth-2*xMargin, mapHeight-borderHeight, |
||||
xMargin, yMargin |
||||
) |
||||
) ; |
||||
labelAreas.put( "overflow", |
||||
new LabelArea( "overflow", |
||||
new Point( borderWidth, mapHeight-borderHeight+yMargin ), |
||||
mapWidth-2*borderWidth, 99999, // nb: unlimited height
|
||||
xMargin, yMargin |
||||
) |
||||
) ; |
||||
} |
||||
|
||||
// figure out what order to create the labels
|
||||
String snippetOrder = config.getProperty( "AUTOCREATE_LABEL_ORDER", |
||||
"scenario players scenario_note* victory_conditions ssr ob_setup_1* ob_note_1* ob_vehicles_1 ob_ordnance_1 ob_setup_2* ob_note_2* ob_vehicles_2 ob_ordnance_2" |
||||
) ; |
||||
logger.debug( "Snippet order: {}", snippetOrder ) ; |
||||
Set<String> snippetsKeySet = new HashSet<String>( snippets.keySet() ) ; |
||||
ArrayList<String> snippetIds = new ArrayList<String>() ; |
||||
for ( String snippetId: snippetOrder.split( "\\s+" ) ) { |
||||
|
||||
if ( snippetId.charAt( snippetId.length()-1 ) == '*' ) { |
||||
|
||||
// this is a wildcard snippet ID - find all matching snippets
|
||||
ArrayList<String> matches = new ArrayList<String>() ; |
||||
String snippetIdStem = snippetId.substring( 0, snippetId.length()-1 ) ; |
||||
Iterator<String> iter2 = snippetsKeySet.iterator() ; |
||||
while( iter2.hasNext() ) { |
||||
String sid = iter2.next() ; |
||||
if ( Utils.startsWith( sid, snippetIdStem ) ) { |
||||
matches.add( sid ) ; |
||||
iter2.remove() ; |
||||
} |
||||
} |
||||
Collections.sort( matches, new Comparator<String>() { |
||||
public int compare( String lhs, String rhs ) { |
||||
// NOTE: These snippet ID's have the form "xyz.1", "xyz.2", etc. - we sort by the trailing number.
|
||||
int pos = lhs.lastIndexOf( '.' ) ; |
||||
int lhsVal = Integer.parseInt( lhs.substring( pos+1 ) ) ; |
||||
pos = rhs.lastIndexOf( '.' ) ; |
||||
int rhsVal = Integer.parseInt( rhs.substring( pos+1 ) ) ; |
||||
if ( lhsVal == rhsVal ) |
||||
return 0 ; |
||||
else |
||||
return lhsVal < rhsVal ? -1 : +1 ; |
||||
} |
||||
} ) ; |
||||
for ( String sid: matches ) |
||||
snippetIds.add( sid ) ; |
||||
|
||||
} else { |
||||
|
||||
// this is a normal snippet ID - add it to the list (if present)
|
||||
if ( snippetsKeySet.contains( snippetId ) ) { |
||||
snippetIds.add( snippetId ) ; |
||||
snippetsKeySet.remove( snippetId ) ; |
||||
} |
||||
|
||||
} |
||||
} |
||||
// add any leftovers
|
||||
for ( String snippetId: snippetsKeySet ) |
||||
snippetIds.add( snippetId ) ; |
||||
|
||||
// create new labels
|
||||
String forceNewRowForVal = config.getProperty( "AUTOCREATE_LABEL_FORCE_NEW_ROW_FOR", |
||||
"ob_setup_1.1 ob_note_1.1 ob_vehicles|ordnance_1 ob_setup_2.1 ob_note_2.1 ob_vehicles|ordnance_2" |
||||
) ; |
||||
logger.debug( "Force new row for: {}", forceNewRowForVal ) ; |
||||
Set<String> forceNewRowFor = new HashSet<String>( |
||||
Arrays.asList( forceNewRowForVal.split( "\\s+" ) ) |
||||
) ; |
||||
logger.info( "Creating labels..." ) ; |
||||
for ( String snippetId: snippetIds ) { |
||||
|
||||
// get the next snippet
|
||||
Snippet snippet = snippets.get( snippetId ) ; |
||||
if ( snippet == null ) { |
||||
logger.info( "- WARNING: Couldn't find a snippet for '{}'.", snippetId ) ; |
||||
continue ; |
||||
} |
||||
if ( ! snippet.autoCreate ) { |
||||
logger.debug( "- Auto-create disabled for '{}'.", snippetId ) ; |
||||
continue ; |
||||
} |
||||
if ( snippet.content.length() == 0 ) { |
||||
logger.info( "- Skipping label creation for '{}' - no content.", snippetId ) ; |
||||
continue ; |
||||
} |
||||
|
||||
// figure out where to put the new label
|
||||
LabelArea labelArea = labelAreas.get( snippet.labelArea ) ; |
||||
if ( labelArea == null ) |
||||
labelArea = labelAreas.get( "general" ) ; |
||||
if ( isForceNewRow( forceNewRowFor, snippetId ) ) |
||||
labelArea.startNewRow( snippetId ) ; |
||||
Point pos = labelArea.getNextPosition( snippetId, snippet.width, snippet.height ) ; |
||||
if ( pos == null ) { |
||||
LabelArea labelArea2 = labelAreas.get( "overflow" ) ; |
||||
pos = labelArea2.getNextPosition( snippetId, snippet.width, snippet.height ) ; |
||||
if ( pos == null ) |
||||
throw new RuntimeException( "LabelArea '" + labelArea.getName() + "' and 'overflow' are full!" ) ; |
||||
} |
||||
|
||||
// create the label
|
||||
// NOTE: This is a bit of a hack :-/ We generate a new GamePiece from the PieceSlot, which gives us a label
|
||||
// loaded with default values. We then replace these default values with our values, and then add
|
||||
// the GamePiece to the game. This will break if the default values ever change, but that's unlikely to happen.
|
||||
logger.info( "- Creating label '{}' at [{},{}].", snippetId, pos.x, pos.y ) ; |
||||
GamePiece gamePiece = labelPieceSlot.getPiece() ; |
||||
gamePiece = PieceCloner.getInstance().clonePiece( gamePiece ) ; // nb: the piece needs to be "expanded"
|
||||
String defaultUserName = config.getProperty( "DEFAULT_LABEL_USERNAME", "David Sullivan" ) ; |
||||
String defaultLabelText1 = config.getProperty( "DEFAULT_LABEL_TEXT1", "Label" ) ; |
||||
String defaultLabelText2 = config.getProperty( "DEFAULT_LABEL_TEXT2", "no background" ) ; |
||||
String snippetContent = snippet.content.replace( "\n", " " ) ; |
||||
gamePiece.setState( |
||||
gamePiece.getState().replace( "\t"+defaultUserName+"\\", "\tvasl-templates\\" ) |
||||
.replace( "\t"+defaultLabelText1+"\\", "\t" + snippetContent + "\\" ) |
||||
.replace( "\t"+defaultLabelText2+"\\", "\t\\" ) |
||||
.replace( "\tnull;0;0", "\tMap0;" + makeVassalCoordString(pos,snippet) ) |
||||
) ; |
||||
GameModule.getGameModule().getGameState().addPiece( gamePiece ) ; |
||||
labelReport.get( "created" ).add( new ReportNode( snippetId, pos ) ) ; |
||||
} |
||||
|
||||
return labelReport ; |
||||
} |
||||
|
||||
private GamePieceLabelFields findLegacyLabel( ArrayList<GamePieceLabelFields> otherLabels, Snippet snippet ) |
||||
{ |
||||
// NOTE: We match snippets with labels via a snippet ID, stored in the HTML fragments in a special
|
||||
// "<!-- vasl-templates:id ... -->" comment. However, for labels created with older versions of vasl-templates,
|
||||
// this comment won't be present, so we try to match labels based on the raw content the user entered
|
||||
// in the UI of the main program.
|
||||
|
||||
// NOTE: Since we are dealing with labels that don't have a snippet ID, the GamePieceLabelField's won't have
|
||||
// their fieldIndex set. We set this if and when we match a legacy label, but we don't handle the case
|
||||
// where some phrases are found in label1 and some in label2 :-/ It doesn't really matter which one we use,
|
||||
// since one of the fields will be used to store the snippet, and the other one will be blanked out.
|
||||
int fieldIndex = -1 ; |
||||
|
||||
// check each label and record which ones match the snippets's raw content
|
||||
ArrayList<GamePieceLabelFields> matches = new ArrayList<GamePieceLabelFields>() ; |
||||
for ( GamePieceLabelFields labelFields: otherLabels ) { |
||||
|
||||
// check if all the snippet raw content phrases are present in the label
|
||||
if ( snippet.rawContent.size() == 0 ) { |
||||
// nb: we can get here for snippets that are always passed through, even if they have no content
|
||||
continue ; |
||||
} |
||||
boolean allFound = true ; |
||||
for ( String phrase: snippet.rawContent ) { |
||||
phrase = phrase.replace( "\n", " " ) ; |
||||
String labelContent = labelFields.getLabelContent( GamePieceLabelFields.FIELD_INDEX_LABEL1 ) ; |
||||
if ( labelContent != null && labelContent.indexOf( phrase ) >= 0 ) { |
||||
fieldIndex = GamePieceLabelFields.FIELD_INDEX_LABEL1 ; |
||||
continue ; |
||||
} |
||||
labelContent = labelFields.getLabelContent( GamePieceLabelFields.FIELD_INDEX_LABEL2 ) ; |
||||
if ( labelContent != null && labelContent.indexOf( phrase ) >= 0 ) { |
||||
fieldIndex = GamePieceLabelFields.FIELD_INDEX_LABEL2 ; |
||||
continue ; |
||||
} |
||||
allFound = false ; |
||||
break ; |
||||
} |
||||
|
||||
// yup - all phrases were found, record the label as a match
|
||||
if ( allFound ) |
||||
matches.add( labelFields ) ; |
||||
} |
||||
|
||||
// NOTE: Exactly one label must match for us to consider it a match (i.e. if there are
|
||||
// multiple matches, we do nothing and leave it to the user to sort it out).
|
||||
if ( matches.size() == 1 ) { |
||||
GamePieceLabelFields labelFields = matches.get( 0 ) ; |
||||
labelFields.setFieldIndex( fieldIndex ) ; |
||||
return labelFields ; |
||||
} |
||||
|
||||
return null ; |
||||
} |
||||
|
||||
private void generateLabelReport( Map<String,ArrayList<ReportNode>> labelReport, String reportFilename ) |
||||
throws TransformerException, TransformerConfigurationException, ParserConfigurationException, FileNotFoundException |
||||
{ |
||||
// generate the report
|
||||
Document doc = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument() ; |
||||
Element rootElem = doc.createElement( "report" ) ; |
||||
doc.appendChild( rootElem ) ; |
||||
boolean wasModified = false ; |
||||
for ( String key: labelReport.keySet() ) { |
||||
ArrayList<ReportNode> reportNodes = labelReport.get( key ) ; |
||||
Element elem = doc.createElement( key ) ; |
||||
for ( ReportNode reportNode: reportNodes ) { |
||||
Element reportNodeElem = doc.createElement( "label" ) ; |
||||
reportNodeElem.setAttribute( "id", reportNode.snippetId ) ; |
||||
if ( reportNode.labelPos != null ) { |
||||
reportNodeElem.setAttribute( "x", Integer.toString( reportNode.labelPos.x ) ) ; |
||||
reportNodeElem.setAttribute( "y", Integer.toString( reportNode.labelPos.y ) ) ; |
||||
} |
||||
elem.appendChild( reportNodeElem ) ; |
||||
if ( ! key.equals( "unchanged" ) ) |
||||
wasModified = true ; |
||||
} |
||||
rootElem.appendChild( elem ) ; |
||||
} |
||||
rootElem.setAttribute( "wasModified", wasModified?"true":"false" ) ; |
||||
|
||||
// save the report
|
||||
Transformer trans = TransformerFactory.newInstance().newTransformer() ; |
||||
trans.setOutputProperty( OutputKeys.INDENT, "yes" ) ; |
||||
trans.setOutputProperty( "{http://xml.apache.org/xslt}indent-amount", "4" ) ; |
||||
trans.setOutputProperty( OutputKeys.METHOD, "xml" ) ; |
||||
trans.setOutputProperty( OutputKeys.ENCODING, "UTF-8" ) ; |
||||
trans.transform( new DOMSource(doc), new StreamResult(new FileOutputStream(reportFilename)) ) ; |
||||
} |
||||
|
||||
private boolean isForceNewRow( Set<String> forceNewRowFor, String snippetId ) |
||||
{ |
||||
// check if we should start a new row when creating labels
|
||||
if ( forceNewRowFor.contains( snippetId ) ) |
||||
return true ; |
||||
|
||||
// FUDGE! To handle the case where an OB has only vehicles or ordnance, we recognize
|
||||
// this special pseudo-snippet ID.
|
||||
if ( Utils.startsWith( snippetId, "ob_vehicles_" ) || Utils.startsWith( snippetId, "ob_ordnance_" ) ) { |
||||
String playerId = snippetId.substring( snippetId.length() - 1 ) ; |
||||
if ( forceNewRowFor.contains( "ob_vehicles|ordnance_" + playerId ) ) { |
||||
// remove the pseudo-snippet ID, so that it doesn't match the other V/O snippet
|
||||
forceNewRowFor.remove( "ob_vehicles|ordnance_" + playerId ) ; |
||||
return true ; |
||||
} |
||||
} |
||||
|
||||
return false ; |
||||
} |
||||
|
||||
private String makeVassalCoordString( Point pos, Snippet snippet ) |
||||
{ |
||||
// FUDGE! VASSAL positions labels by the X/Y co-ords of the label's centre (!)
|
||||
return Integer.toString( pos.x + snippet.width/2 ) + ";" + Integer.toString( pos.y + snippet.height/2 ) ; |
||||
} |
||||
|
||||
private void saveScenario( String saveFilename ) throws IOException |
||||
{ |
||||
// disable the dialog asking for log file comments
|
||||
Prefs prefs = GameModule.getGameModule().getPrefs() ; |
||||
String PROMPT_LOG_COMMENT = "promptLogComment"; |
||||
prefs.setValue( PROMPT_LOG_COMMENT, false ) ; |
||||
|
||||
// FUDGE! We would like to just call GameState.saveGame(), but it calls getRestoreCommand(),
|
||||
// which does nothing unless the "save game" menu action has been enabled!?! Due to Java protections,
|
||||
// there doesn't seem to be any way to get at this object and enable it, so we have to re-implement
|
||||
// the whole saveGame() code without this check :-/
|
||||
|
||||
// get the save string
|
||||
Command cmd = getRestoreCommand() ; |
||||
String saveString = GameModule.getGameModule().encode( cmd ) ; |
||||
|
||||
// save the scenario
|
||||
logger.info( "Saving scenario: {}", saveFilename ) ; |
||||
final FastByteArrayOutputStream ba = new FastByteArrayOutputStream() ; |
||||
OutputStream out = null ; |
||||
try { |
||||
out = new ObfuscatingOutputStream( ba ) ; |
||||
out.write( saveString.getBytes( "UTF-8" ) ) ; |
||||
out.close() ; |
||||
} |
||||
finally { |
||||
IOUtils.closeQuietly( out ) ; |
||||
} |
||||
FileArchive archive = null ; |
||||
try { |
||||
archive = new ZipArchive( new File( saveFilename ) ) ; |
||||
String SAVEFILE_ZIP_ENTRY = "savedGame" ; //$NON-NLS-1$
|
||||
archive.add( SAVEFILE_ZIP_ENTRY, ba.toInputStream() ) ; |
||||
(new SaveMetaData()).save( archive ) ; |
||||
archive.close() ; |
||||
} |
||||
finally { |
||||
IOUtils.closeQuietly( archive ) ; |
||||
} |
||||
} |
||||
|
||||
private static Command getRestoreCommand() // nb: taken from GameState.getRestoreCommand()
|
||||
{ |
||||
// NOTE: This is the check that's causing the problem :-/
|
||||
// if (!saveGame.isEnabled()) {
|
||||
// return null;
|
||||
// }
|
||||
|
||||
GameState gameState = GameModule.getGameModule().getGameState() ; |
||||
Command c = new GameState.SetupCommand(false); |
||||
c.append(checkVersionCommand()); |
||||
c.append( gameState.getRestorePiecesCommand() ); |
||||
for (GameComponent gc : gameState.getGameComponents()) { |
||||
c.append(gc.getRestoreCommand()); |
||||
} |
||||
c.append(new GameState.SetupCommand(true)); |
||||
return c; |
||||
} |
||||
|
||||
private static Command checkVersionCommand() { |
||||
// NOTE: This is the same as GameState.checkVersionCommand(), but we can't call that since it's private :-/
|
||||
String runningVersion = GameModule.getGameModule().getAttributeValueString(GameModule.VASSAL_VERSION_RUNNING); |
||||
ConditionalCommand.Condition cond = new ConditionalCommand.Lt(GameModule.VASSAL_VERSION_RUNNING, runningVersion); |
||||
Command c = new ConditionalCommand(new ConditionalCommand.Condition[]{cond}, new AlertCommand(Resources.getString("GameState.version_mismatch", runningVersion))); //$NON-NLS-1$
|
||||
String moduleName = GameModule.getGameModule().getAttributeValueString(GameModule.MODULE_NAME); |
||||
String moduleVersion = GameModule.getGameModule().getAttributeValueString(GameModule.MODULE_VERSION); |
||||
cond = new ConditionalCommand.Lt(GameModule.MODULE_VERSION, moduleVersion); |
||||
c.append(new ConditionalCommand(new ConditionalCommand.Condition[]{cond}, new AlertCommand(Resources.getString("GameState.version_mismatch2", moduleName, moduleVersion )))); //$NON-NLS-1$
|
||||
return c; |
||||
} |
||||
|
||||
private Command loadScenario( String scenarioFilename ) throws IOException |
||||
{ |
||||
// load the scenario
|
||||
disableBoardWarnings() ; |
||||
logger.info( "Loading scenario: {}", scenarioFilename ) ; |
||||
return GameModule.getGameModule().getGameState().decodeSavedGame( |
||||
new File( scenarioFilename ) |
||||
) ; |
||||
} |
||||
|
||||
private static void dumpCommand( Command cmd, String prefix ) |
||||
{ |
||||
// dump the command
|
||||
StringBuilder buf = new StringBuilder() ; |
||||
buf.append( prefix + cmd.getClass().getSimpleName() ) ; |
||||
String details = cmd.getDetails() ; |
||||
if ( details != null ) |
||||
buf.append( " [" + details + "]" ) ; |
||||
if ( cmd instanceof AddPiece ) |
||||
dumpCommandExtras( (AddPiece)cmd, buf, prefix ) ; |
||||
else if ( cmd instanceof GameState.SetupCommand ) |
||||
dumpCommandExtras( (GameState.SetupCommand)cmd, buf, prefix ) ; |
||||
else if ( cmd instanceof ModuleExtension.RegCmd ) |
||||
dumpCommandExtras( (ModuleExtension.RegCmd)cmd, buf, prefix ) ; |
||||
else if ( cmd instanceof ObscurableOptions.SetAllowed ) |
||||
dumpCommandExtras( (ObscurableOptions.SetAllowed)cmd, buf, prefix ) ; |
||||
System.out.println( buf.toString() ) ; |
||||
|
||||
// dump any sub-commands
|
||||
prefix += " " ; |
||||
for ( Command c: cmd.getSubCommands() ) |
||||
dumpCommand( c, prefix ) ; |
||||
} |
||||
|
||||
private static void dumpCommandExtras( AddPiece cmd, StringBuilder buf, String prefix ) |
||||
{ |
||||
// dump extra command info
|
||||
GamePiece target = cmd.getTarget() ; |
||||
buf.append( ": " + target.getClass().getSimpleName() ) ; |
||||
if ( target.getName().length() > 0 ) |
||||
buf.append( "/" + target.getName() ) ; |
||||
|
||||
// check if this is a command we're interested in
|
||||
// NOTE: We used to support VASL 6.3.3, but when we create labels, they're of type Hideable. It would be easy enough
|
||||
// to add that here, but 6.3.3 is pretty old (2.5 years), so it's safer to just drop it from the list of supported versions.
|
||||
if ( !( target instanceof DynamicProperty ) ) |
||||
return ; |
||||
if ( ! target.getName().equals( "User-Labeled" ) ) |
||||
return ; |
||||
|
||||
// dump extra command info
|
||||
ArrayList<String> separators = new ArrayList<String>() ; |
||||
ArrayList<String> fields = new ArrayList<String>() ; |
||||
parseGamePieceState( cmd.getState(), separators, fields ) ; |
||||
for ( String field: fields ) { |
||||
buf.append( "\n" + prefix + "- " ) ; |
||||
if ( field.length() > 0 ) |
||||
buf.append( Utils.printableString( field ) ) ; |
||||
else |
||||
buf.append( "<empty>" ) ; |
||||
} |
||||
} |
||||
|
||||
private static void dumpCommandExtras( GameState.SetupCommand cmd, StringBuilder buf, String prefix ) |
||||
{ |
||||
// dump extra command info
|
||||
buf.append( ": starting=" + cmd.isGameStarting() ) ; |
||||
} |
||||
|
||||
private static void dumpCommandExtras( ModuleExtension.RegCmd cmd, StringBuilder buf, String prefix ) |
||||
{ |
||||
// dump extra command info
|
||||
buf.append( ": " + cmd.getName() + " (" + cmd.getVersion() + ")" ) ; |
||||
} |
||||
|
||||
private static void dumpCommandExtras( ObscurableOptions.SetAllowed cmd, StringBuilder buf, String prefix ) |
||||
{ |
||||
// dump extra command info
|
||||
buf.append( ": " + cmd.getAllowedIds() ) ; |
||||
} |
||||
|
||||
private static void parseGamePieceState( String state, ArrayList<String> separators, ArrayList<String> fields ) |
||||
{ |
||||
// parse the GamePiece state
|
||||
Matcher matcher = Pattern.compile( "\\\\+\t" ).matcher( state ) ; |
||||
int pos = 0 ; |
||||
while( matcher.find() ) { |
||||
separators.add( matcher.group() ) ; |
||||
fields.add( state.substring( pos, matcher.start() ) ) ; |
||||
pos = matcher.end() ; |
||||
} |
||||
fields.add( state.substring( pos ) ) ; |
||||
} |
||||
|
||||
private void disableBoardWarnings() |
||||
{ |
||||
// FUDGE! VASSAL shows a GUI error dialog warning about boards not being found, and while these can be disabled,
|
||||
// the key used to enable/disable them is derived from the board filename :-( ASLBoardPicker catches
|
||||
// the FileNotFoundException thrown by ZipArchive when it can't find a file, and then calls ReadErrorDialog.error(),
|
||||
// which calls WarningDialog.showDisableable(), using the following as the key:
|
||||
// (Object) ( e.getClass().getName() + "@" + filename )
|
||||
// This means we have to set the "warning disabled" flag for every possible board :-/
|
||||
|
||||
// disable warnings for boards 00-99
|
||||
logger.info( "Disabling board warnings for bd00-99." ) ; |
||||
for ( int i=0 ; i < 100 ; ++i ) |
||||
disableBoardWarning( String.format( "bd%02d", i ) ) ; |
||||
|
||||
// disable warnings for additional standard boards
|
||||
logger.info( "Disabling board warnings for other standard boards:" ) ; |
||||
InputStream inputStream = this.getClass().getResourceAsStream( "/data/boardNames.txt" ) ; |
||||
disableBoardWarnings( inputStream, "<standard>" ) ; |
||||
|
||||
// disable warnings for user-defined boards
|
||||
if ( baseDir != null ) { |
||||
String fname = baseDir + File.separator + "boardNames.txt" ; |
||||
inputStream = null ; |
||||
try { |
||||
inputStream = new FileInputStream( fname ) ; |
||||
} catch( FileNotFoundException ex ) { } |
||||
if ( inputStream != null ) { |
||||
logger.info( "Disabling board warnings for user-defined boards: " + fname ) ; |
||||
disableBoardWarnings( inputStream, fname ) ; |
||||
} |
||||
} |
||||
} |
||||
|
||||
private void disableBoardWarnings( InputStream inputStream, String boardFilename ) |
||||
{ |
||||
// disable warnings for boards listed in a file
|
||||
BufferedReader reader = new BufferedReader( new InputStreamReader( inputStream ) ) ; |
||||
String lineBuf ; |
||||
try { |
||||
while ( (lineBuf = reader.readLine() ) != null ) { |
||||
lineBuf = lineBuf.trim() ; |
||||
if ( lineBuf.length() == 0 || lineBuf.charAt(0) == '#' || lineBuf.charAt(0) == ';' || lineBuf.substring(0,2).equals("//") ) |
||||
continue ; |
||||
logger.debug( "- {}", lineBuf ) ; |
||||
disableBoardWarning( lineBuf ) ; |
||||
} |
||||
} catch( IOException ex ) { |
||||
logger.error( "Error reading board file: {}", boardFilename, ex ) ; |
||||
} |
||||
} |
||||
|
||||
private void disableBoardWarning( String boardName ) |
||||
{ |
||||
// disable warnings for the specified board
|
||||
String boardsPath = (new File(vmodFilename)).getParent() + File.separator + "boards" ; |
||||
String key = "java.io.FileNotFoundException@" + boardsPath + File.separator + boardName ; |
||||
DialogUtils.setDisabled( key, true ) ; |
||||
} |
||||
} |
Loading…
Reference in new issue