Added log file analysis.

master
Pacman Ghost 4 years ago
parent 0d0fe5869a
commit c043a892f0
  1. 66
      vasl_templates/main_window.py
  2. 268
      vasl_templates/tools/dump_log_file_analysis.py
  3. 15
      vasl_templates/web_channel.py
  4. 1
      vasl_templates/webapp/__init__.py
  5. 3
      vasl_templates/webapp/config/logging.yaml.example
  6. 14
      vasl_templates/webapp/data/default-template-pack/scenario.j2
  7. 154
      vasl_templates/webapp/lfa.py
  8. 21
      vasl_templates/webapp/main.py
  9. 284
      vasl_templates/webapp/static/LogFileAnalysis.js
  10. 1
      vasl_templates/webapp/static/chartjs/Chart.min.css
  11. 7
      vasl_templates/webapp/static/chartjs/Chart.min.js
  12. 25
      vasl_templates/webapp/static/chartjs/chartjs-plugin-labels.min.js
  13. 37
      vasl_templates/webapp/static/css/lfa-upload.css
  14. 113
      vasl_templates/webapp/static/css/lfa.css
  15. 1
      vasl_templates/webapp/static/css/user-settings-dialog.css
  16. BIN
      vasl_templates/webapp/static/images/gripper.png
  17. BIN
      vasl_templates/webapp/static/images/lfa/die/white/1.png
  18. BIN
      vasl_templates/webapp/static/images/lfa/die/white/2.png
  19. BIN
      vasl_templates/webapp/static/images/lfa/die/white/3.png
  20. BIN
      vasl_templates/webapp/static/images/lfa/die/white/4.png
  21. BIN
      vasl_templates/webapp/static/images/lfa/die/white/5.png
  22. BIN
      vasl_templates/webapp/static/images/lfa/die/white/6.png
  23. BIN
      vasl_templates/webapp/static/images/lfa/die/yellow/1.png
  24. BIN
      vasl_templates/webapp/static/images/lfa/die/yellow/2.png
  25. BIN
      vasl_templates/webapp/static/images/lfa/die/yellow/3.png
  26. BIN
      vasl_templates/webapp/static/images/lfa/die/yellow/4.png
  27. BIN
      vasl_templates/webapp/static/images/lfa/die/yellow/5.png
  28. BIN
      vasl_templates/webapp/static/images/lfa/die/yellow/6.png
  29. BIN
      vasl_templates/webapp/static/images/lfa/download.png
  30. BIN
      vasl_templates/webapp/static/images/lfa/file.png
  31. BIN
      vasl_templates/webapp/static/images/lfa/hotness.png
  32. BIN
      vasl_templates/webapp/static/images/lfa/minus.png
  33. BIN
      vasl_templates/webapp/static/images/lfa/player-colors.png
  34. BIN
      vasl_templates/webapp/static/images/lfa/plus.png
  35. 26
      vasl_templates/webapp/static/jQueryHandlers.js
  36. 288
      vasl_templates/webapp/static/lfa-upload.js
  37. 1844
      vasl_templates/webapp/static/lfa.js
  38. 7
      vasl_templates/webapp/static/main.js
  39. 507
      vasl_templates/webapp/static/spectrum/spectrum.css
  40. 2342
      vasl_templates/webapp/static/spectrum/spectrum.js
  41. 3
      vasl_templates/webapp/static/split/split.min.js
  42. 20
      vasl_templates/webapp/static/user_settings.js
  43. 16
      vasl_templates/webapp/static/utils.js
  44. 42
      vasl_templates/webapp/static/vassal.js
  45. 23
      vasl_templates/webapp/templates/index.html
  46. 13
      vasl_templates/webapp/templates/lfa-upload.html
  47. 76
      vasl_templates/webapp/templates/lfa.html
  48. 2
      vasl_templates/webapp/templates/testing.html
  49. 7
      vasl_templates/webapp/templates/user-settings-dialog.html
  50. BIN
      vasl_templates/webapp/tests/fixtures/analyze-vlog/3d6.vlog
  51. BIN
      vasl_templates/webapp/tests/fixtures/analyze-vlog/4players.vlog
  52. BIN
      vasl_templates/webapp/tests/fixtures/analyze-vlog/banner-updates.vlog
  53. BIN
      vasl_templates/webapp/tests/fixtures/analyze-vlog/download-test.vlog
  54. BIN
      vasl_templates/webapp/tests/fixtures/analyze-vlog/empty.vlog
  55. BIN
      vasl_templates/webapp/tests/fixtures/analyze-vlog/full.vlog
  56. BIN
      vasl_templates/webapp/tests/fixtures/analyze-vlog/multiple-1.vlog
  57. BIN
      vasl_templates/webapp/tests/fixtures/analyze-vlog/multiple-1a.vlog
  58. BIN
      vasl_templates/webapp/tests/fixtures/analyze-vlog/multiple-1b.vlog
  59. BIN
      vasl_templates/webapp/tests/fixtures/analyze-vlog/multiple-2.vlog
  60. 589
      vasl_templates/webapp/tests/test_lfa.py
  61. 2
      vasl_templates/webapp/tests/test_template_packs.py
  62. 2
      vasl_templates/webapp/tests/test_vasl_extensions.py
  63. 4
      vasl_templates/webapp/tests/test_vassal.py
  64. 49
      vasl_templates/webapp/tests/utils.py
  65. 32
      vasl_templates/webapp/utils.py
  66. 30
      vasl_templates/webapp/vassal.py
  67. 4
      vasl_templates/webapp/webdriver.py
  68. BIN
      vassal-shim/release/vassal-shim.jar
  69. 31
      vassal-shim/src/vassal_shim/Main.java
  70. 11
      vassal-shim/src/vassal_shim/Utils.java
  71. 170
      vassal-shim/src/vassal_shim/VassalShim.java
  72. 37
      vassal-shim/src/vassal_shim/lfa/DiceEvent.java
  73. 12
      vassal-shim/src/vassal_shim/lfa/Event.java
  74. 20
      vassal-shim/src/vassal_shim/lfa/LogFileAnalysis.java
  75. 37
      vassal-shim/src/vassal_shim/lfa/TurnTrackEvent.java

@ -4,7 +4,6 @@ import sys
import os
import re
import json
import io
import base64
import logging
@ -87,7 +86,7 @@ class MainWindow( QWidget ):
self.restoreGeometry( val )
else :
self.resize( 1050, 650 )
self.setMinimumSize( 1000, 595 )
self.setMinimumSize( 1050, 620 )
# initialize the layout
layout = QVBoxLayout( self )
@ -215,27 +214,29 @@ class MainWindow( QWidget ):
NOTE: This handler might be called multiple times.
"""
def decode_val( val ):
"""Decode a settings value."""
# NOTE: Comma-separated values are deserialized as lists automatically.
if val == "true":
return True
if val == "false":
return False
if str(val).isdigit():
return int(val)
return val
# load and install the user settings
buf = io.StringIO()
buf.write( "{" )
user_settings = {}
for key in app_settings.allKeys():
if key.startswith( "UserSettings/" ):
val = app_settings.value(key)
if val in ("true","false") or val.isdigit():
buf.write( '"{}": {},'.format( key[13:], val ) )
else:
buf.write( '"{}": "{}",'.format( key[13:], val ) )
buf.write( '"_dummy_": null }' )
buf = buf.getvalue()
user_settings = {}
try:
user_settings = json.loads( buf )
except Exception as ex: #pylint: disable=broad-except
MainWindow.showErrorMsg( "Couldn't load the user settings:\n\n{}".format( ex ) )
logging.error( "Couldn't load the user settings: %s", ex )
logging.error( buf )
return
del user_settings["_dummy_"]
val = app_settings.value( key )
key = key[13:] # remove the leading "UserSettings/"
sections = key.split( "." )
target = user_settings
while len(sections) > 1:
if sections[0] not in target:
target[ sections[0] ] = {}
target = target[ sections.pop(0) ]
target[ sections[0] ] = decode_val( val )
self._view.page().runJavaScript(
"install_user_settings('{}')".format( json.dumps( user_settings ) )
)
@ -284,6 +285,12 @@ class MainWindow( QWidget ):
data = base64.b64decode( data )
return self._web_channel_handler.save_updated_vsav( fname, data )
@pyqtSlot( str )
@catch_exceptions( caption="SLOT EXCEPTION", retval=False )
def save_log_file_analysis( self, data ):
"""Called when the user wants to save a log file analysis."""
self._web_channel_handler.save_log_file_analysis( data )
@pyqtSlot( str )
@catch_exceptions( caption="SLOT EXCEPTION" )
def on_user_settings_change( self, user_settings ): #pylint: disable=no-self-use
@ -293,9 +300,22 @@ class MainWindow( QWidget ):
if key.startswith( "UserSettings/" ):
app_settings.remove( key )
# save the new user settings
user_settings = json.loads( user_settings )
for key,val in user_settings.items():
app_settings.setValue( "UserSettings/{}".format(key), val )
def save_section( vals, key_prefix ):
"""Save a section of the User Settings."""
for key,val in vals.items():
if isinstance( val, dict ):
# FUDGE! The PyQt doco claims that it supports nested sections, but key names that have
# a slash in them get saved as a top-level key, with the slash converted to a back-slash,
# even on Linux :-/ We use dotted key names to represent nested levels.
save_section( val, key_prefix+key+"." )
continue
# NOTE: PyQt handles lists automatically, converting them to a comma-separated list,
# and de-serializing them as lists (string values with a comma in them get quoted).
app_settings.setValue(
"UserSettings/{}".format( key_prefix + key ),
val
)
save_section( json.loads( user_settings ), "" )
@pyqtSlot( str, bool )
@catch_exceptions( caption="SLOT EXCEPTION" )

@ -0,0 +1,268 @@
#!/usr/bin/env python3
"""Dump the log file analysis reports generated by the VASSAL shim."""
import os
import itertools
import click
import tabulate
from vasl_templates.webapp.lfa import parse_analysis_report, DEFAULT_LFA_DICE_HOTNESS_WEIGHTS
EXPECTED_DISTRIB = {
"DR": { 2: 2.8, 3: 5.6, 4: 8.3, 5: 11.1, 6: 13.9, 7: 16.7, 8: 13.9, 9: 11.1, 10: 8.3, 11: 5.6, 12: 2.8 },
"dr": { 1: 16.7, 2: 16.7, 3: 16.7, 4: 16.7, 5: 16.7, 6: 16.7 },
}
# ---------------------------------------------------------------------
@click.command()
@click.option( "--file","-f","fname", required=True, help="Log file analysis report." )
@click.option( "--players/--no-players","-p", help="Dump the players." )
@click.option( "--events/--no-events","-e", help="Dump the events." )
@click.option( "--roll-type","-r","roll_type", help="Roll type filter (e.g. IFT or MC)." )
@click.option( "--window","-w","window_size", default=1, help="Moving average window size." )
def main( fname, players, events, roll_type, window_size ):
"""Dump a Log File Analysis report (generated by the VASSAL shim)."""
# initialize
if not os.path.isfile( fname ):
raise RuntimeError( "Can't find the report file: {}".format( fname ) )
# parse the report
report = parse_analysis_report( fname )
# dump each log file
for log_file in report["logFiles"]:
# output a header for the next log file
print( "=== {} {}".format( log_file["filename"], 80*"=" )[ :80 ] )
print()
# dump the scenario details
scenario_name = log_file["scenario"].get( "scenarioName" )
if scenario_name:
print( "Scenario: {}".format( scenario_name ), end="" )
scenario_id = log_file["scenario"].get( "scenarioId" )
if scenario_id:
print( " ({})".format( scenario_id ), end="" )
print()
# dump the players
if players:
print( "Players:" )
max_id_len = max( len(k) for k in report["players"] )
fmt = "- {:%d} = {}" % max_id_len
for player_id,player_name in report["players"].items():
print( fmt.format( player_id, player_name ) )
# dump the DR/dr distributions
dump_distrib( report["players"], log_file, roll_type )
print()
# dump the time-plot
if events or roll_type:
dump_time_plot( report["players"], log_file, roll_type, window_size )
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
def dump_distrib( players, log_file, roll_type ): #pylint: disable=too-many-locals,too-many-branches,too-many-statements
"""Dump the DR/dr distributions."""
# initialize
stats = { p: {
"DR": { "nRolls": 0, "rollTotal": 0 },
"dr": { "nRolls": 0, "rollTotal": 0 },
} for p in players
}
distrib = { p: {
"DR": { k: 0 for k in range(2,12+1) },
"dr": { k: 0 for k in range(1,6+1) },
} for p in players
}
# process events
for evt in log_file["events"]:
# check if we should process the next event
if evt["eventType"] != "roll":
continue
if roll_type and evt["rollType"].lower() != roll_type.lower():
continue
# update the stats
player_id = evt["playerId"]
key = "DR" if isinstance( evt["rollValue"], list ) else "dr"
stats[ player_id ][ key ][ "nRolls" ] += 1
val = roll_total( evt["rollValue"] )
stats[ player_id ][ key ][ "rollTotal" ] += val
distrib[ player_id ][ key ][ val ] += 1
# calculate averages
avg = lambda x, y: x / y if y != 0 else 0
for player_id in players:
for key in ["DR","dr"]:
stats[ player_id ][ key ][ "rollAverage" ] = avg(
stats[player_id][key].pop("rollTotal"),
stats[player_id][key]["nRolls"]
)
# calculate chi-squared and hotness
for player_id in players:
for key in ["DR","dr"]:
stats[ player_id ][ key ][ "chiSquared" ] = chi_squared(
distrib[player_id][ key ],
EXPECTED_DISTRIB[ key ]
)
stats[ player_id ][ key ][ "hotness" ] = hotness(
distrib[player_id][ key ],
EXPECTED_DISTRIB[ key ],
DEFAULT_LFA_DICE_HOTNESS_WEIGHTS[ key ],
)
# output the results
for key in ["dr","DR"]:
print()
print( "=== {} distribution ===".format( key ) )
vals = range(2,12+1) if key == "DR" else range(1,6+1)
results = [ itertools.chain( [""], vals, ["total","average","chi2","hotness"] ) ]
total_rolls = sum( stats[p][key]["nRolls"] for p in players )
for player_id,player_name in players.items():
# add a row for the stats
row = [ player_name ]
has_vals = False
for val in vals:
nRolls = distrib[player_id][key][val]
if nRolls != 0:
row.append( nRolls )
has_vals = True
else:
row.append( "" )
val2 = stats[player_id][key]["nRolls"]
val2a = val2 / total_rolls if total_rolls != 0 else 0
row.append( "{} ({}%)".format( val2, int(100*val2a+0.5) ) )
row.append( fpfmt( stats[player_id][key]["rollAverage"], 1 ) )
row.append( fpfmt( stats[player_id][key]["chiSquared"], 3 ) )
results.append( row )
# add a row for the averages
if has_vals:
row = [ "" ]
for val in vals:
nRolls = distrib[player_id][key][val]
if nRolls:
val2 = avg( distrib[player_id][key][val], stats[player_id][key]["nRolls"] )
row.append( fpfmt( 100*val2, 1 ) )
else:
row.append( "" )
results.append( row )
# add a row for the dice hotness
row = [ "" ]
partials = stats[ player_id ][ key ][ "hotness" ]
if partials:
for val in partials:
row.append( fpfmt( val, 3 ) )
row.extend( [ "", "", "" ] )
row.append( fpfmt( sum(partials), 3 ) )
results.append( row )
print( tabulate.tabulate( results, headers="firstrow" ) )
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
def dump_time_plot( players, log_file, roll_type, window_size ):
"""Dump the time-plot values."""
# initialize
rolls = []
windows = { p: [] for p in players }
def dump_rolls():
"""Dump the buffered ROLL events."""
print( tabulate.tabulate( rolls, tablefmt="plain" ) )
def onTurnTrack( evt ): #pylint: disable=unused-variable
"""Process a TURN TRACK event."""
nonlocal rolls
if rolls:
dump_rolls()
rolls = []
print()
print( "--- {} Turn {} {} ---".format( evt["side"], evt["turnNo"], evt["phase"] ) )
print()
def onRoll( evt ) : #pylint: disable=unused-variable
"""Process a ROLL event"""
# check if we should process this ROLL event
if roll_type:
if evt["rollType"].lower() != roll_type.lower():
return
player_id = evt[ "playerId" ]
if window_size == 1:
# add the raw roll
if isinstance( evt["rollValue"], int ):
val = evt["rollValue"]
else:
val = ", ".join( str(v) for v in evt["rollValue"] )
rolls.append( [ players[player_id], evt["rollType"], val ] )
else:
# add the moving average
windows[ player_id ].append( roll_total( evt["rollValue"] ) )
if len(windows[player_id]) < window_size:
return
val = sum( windows[player_id] ) / len(windows[player_id])
del windows[player_id][0]
rolls.append( [ players[player_id], val ] )
# process events
print( "=== EVENTS ===" )
print()
for evt in log_file["events"]:
eventType = evt["eventType"]
locals()[ "on" + eventType[0].upper() + eventType[1:] ]( evt )
if rolls:
dump_rolls()
# ---------------------------------------------------------------------
def chi_squared( observed, expected ):
"""Calculate the chi-squared for a set of values."""
nRolls = sum( observed.values() )
if nRolls == 0:
return None
assert observed.keys() == expected.keys()
return sum(
( observed[val]/nRolls - expected[val]/100 ) ** 2 / (expected[val]/100)
for val in expected
)
def hotness( observed, expected, weights ):
"""Calculate the hotness for a set of values."""
nRolls = sum( observed.values() )
if nRolls == 0:
return None
assert observed.keys() == expected.keys() == weights.keys()
partials = []
sign = lambda val: -1 if val < 0 else +1
for val in expected:
diff = observed[val]/nRolls - expected[val]/100
partials.append( sign(diff) * diff**2 * weights[val] / (expected[val]/100) )
return partials
def roll_total( roll ):
"""Calculate the total of a roll."""
if isinstance( roll, list ):
assert all( isinstance(r,int) for r in roll )
return sum( roll )
else:
assert isinstance( roll, int )
return roll
def fpfmt( val, nDigits ):
"""Format a floating point number."""
if val is None:
return "-"
return ("{:.%df}" % nDigits).format( val )
# ---------------------------------------------------------------------
if __name__ == "__main__":
main() #pylint: disable=no-value-for-parameter

@ -28,6 +28,12 @@ class WebChannelHandler:
"VASL scenario files (*.vsav);;All files (*)",
"scenario.vsav"
)
self.log_file_analysis_dialog = FileDialog(
self.parent,
"log file analysis", ".csv",
"Analysis files (*.csv);;All files (*)",
None
)
def on_new_scenario( self ):
"""Called when the scenario is reset."""
@ -82,3 +88,12 @@ class WebChannelHandler:
dname = os.path.split( self.updated_vsav_file_dialog.curr_fname )[0]
self.updated_vsav_file_dialog.curr_fname = os.path.join( dname, fname )
return self.updated_vsav_file_dialog.save_file( data )
def save_log_file_analysis( self, data ):
"""Called when the user wants to save a log file analysis."""
prev_curr_fname = self.log_file_analysis_dialog.curr_fname
if not self.log_file_analysis_dialog.curr_fname:
self.log_file_analysis_dialog.curr_fname = "analysis.csv"
rc = self.log_file_analysis_dialog.save_file( data )
if not rc:
self.log_file_analysis_dialog.curr_fname = prev_curr_fname

@ -99,6 +99,7 @@ import vasl_templates.webapp.vassal #pylint: disable=cyclic-import
import vasl_templates.webapp.vo_notes #pylint: disable=cyclic-import
import vasl_templates.webapp.nat_caps #pylint: disable=cyclic-import
import vasl_templates.webapp.roar #pylint: disable=cyclic-import
import vasl_templates.webapp.lfa #pylint: disable=cyclic-import
if app.config.get( "ENABLE_REMOTE_TEST_CONTROL" ):
print( "*** WARNING: Remote test control enabled! ***" )
import vasl_templates.webapp.testing #pylint: disable=cyclic-import

@ -36,6 +36,9 @@ loggers:
analyze_vsav:
level: "WARNING"
handlers: [ "file" ]
analyze_vlog:
level: "WARNING"
handlers: [ "file" ]
webdriver:
level: "WARNING"
handlers: [ "file" ]

@ -1,5 +1,5 @@
<html> <!-- vasl-templates:id {{SNIPPET_ID}} -->
{%if APP_NAME%} <!-- Generated by {{APP_NAME}} {{APP_VERSION}}:
{%if APP_NAME%}<!-- Generated by {{APP_NAME}} {{APP_VERSION}}:
Time: {{TIMESTAMP}}
VASSAL:{%if VASSAL_VERSION %} {{VASSAL_VERSION}} {%else%} - {%endif%}
VASL: {%if VASL_VERSION%} {{VASL_VERSION}} {%else%} - {%endif%}
@ -9,7 +9,9 @@
<meta charset="utf-8">
<style>
{{CSS:common}}
.small { font-size: 75% ; font-style: italic ; }
.scenario-name { font-weight: bold ; }
.scenario-id { font-size: 75% ; font-style: italic ; }
.scenario-date { font-size: 75% ; font-style: italic ; }
</style>
</head>
@ -24,12 +26,12 @@
border-bottom: 1px solid #c0c0c0 ;
">
<div style="font-size:115%;">
<b> {%if SCENARIO_NAME%} {{SCENARIO_NAME}} {%else%} Untitled scenario {%endif%} </b>
{%if SCENARIO_ID%} <span class="small"> ({{SCENARIO_ID}}) </span> {%endif%}
<span class="scenario-name"> {%if SCENARIO_NAME%} {{SCENARIO_NAME}} {%else%} Untitled scenario {%endif%} </span>
{%if SCENARIO_ID%} <span class="scenario-id"> ({{SCENARIO_ID}}) </span> {%endif%}
</div>
{%if SCENARIO_LOCATION%} {{SCENARIO_LOCATION}} {%endif%}
{%if SCENARIO_LOCATION%} <span class="location"> {{SCENARIO_LOCATION}} </span> {%endif%}
{%if SCENARIO_DATE%}
<span class="small"> ({{SCENARIO_DAY_OF_MONTH_POSTFIX}} {{SCENARIO_MONTH_NAME}}, {{SCENARIO_YEAR}}) </span>
<span class="scenario-date"> ({{SCENARIO_DAY_OF_MONTH_POSTFIX}} {{SCENARIO_MONTH_NAME}}, {{SCENARIO_YEAR}}) </span>
{%endif%}
</table>

@ -0,0 +1,154 @@
""" Webapp handlers. """
import os
import time
import base64
import logging
import xml.etree.cElementTree as ET
from flask import request, jsonify
from vasl_templates.webapp import app
from vasl_templates.webapp.vassal import VassalShim
from vasl_templates.webapp.utils import SimpleError, TempFile
# weights for each possible roll value
DEFAULT_LFA_DICE_HOTNESS_WEIGHTS = {
"DR": { 2: 30, 3: 18, 4: 10, 5: 5, 6: 2, 7: 0, 8: -2, 9: -5, 10: -10, 11: -18, 12: -30 },
"dr": { 1: 5, 2: 2.5, 3: 1, 4: -1, 5: -2.5, 6: -5 }
}
# minimum number of rolls for dice hotness to be considered reasonable
DEFAULT_LFA_DICE_HOTNESS_THRESHOLDS = {
"DR": 100, "dr": 50
}
# ---------------------------------------------------------------------
@app.route( "/analyze-vlogs", methods=["POST"] )
def analyze_vlogs(): #pylint: disable=too-many-locals
"""Analyze VASL log file(s)."""
# parse the request
start_time = time.time()
vlog_data = request.json
# initialize
logger = logging.getLogger( "analyze_vlogs" )
temp_files = []
try:
# save each VLOG file in a temp file
if not vlog_data:
raise SimpleError( "No log files were submitted." )
for vlog_no, vlog in enumerate( vlog_data ):
fname, data = vlog
data = base64.b64decode( data )
logger.info( "Analyzing VLOG (#bytes=%d): %s", len(data), fname )
temp_file = TempFile()
temp_file.open()
temp_file.write( data )
temp_file.close( delete=False )
save_fname = app.config.get( "ANALYZE_VLOG_INPUT" )
if save_fname:
if len(vlog_data) == 1:
temp_file.save_copy( save_fname, logger, "VLOG data" )
else:
parts = os.path.splitext( save_fname )
temp_file.save_copy( "{}-{}".format(parts[0],1+vlog_no) + parts[1], logger, "VLOG data" )
temp_files.append( temp_file )
# run the VASSAL shim to analyze the VLOG file(s)
with TempFile() as report_file:
report_file.close( delete=False )
vassal_shim = VassalShim()
fnames = [ tf.name for tf in temp_files ]
fnames.append( report_file.name )
vassal_shim.analyze_logfiles( *fnames )
report_file.save_copy( app.config.get("ANALYZE_VLOG_REPORT"), logger, "analysis report" )
report = parse_analysis_report( report_file.name, logger )
except Exception as ex: #pylint: disable=broad-except
return VassalShim.translate_vassal_shim_exception( ex, logger )
finally:
# clean up
for tf in temp_files:
tf.close( delete=True )
# insert the filenames for each log file, as they were passed in to us
for vlog_no,vlog in enumerate( vlog_data ):
report["logFiles"][ vlog_no ]["filename"] = vlog[0]
# return the results
logger.info( "Analyzed the VLOG file(s) OK: elapsed=%.3fs", time.time()-start_time )
return jsonify( report )
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
def parse_analysis_report( fname, logger=None ):
"""Parse the analysis report generated by the VASSAL shim."""
# initialize
doc = ET.parse( fname )
# get the complete list of players across all the log files
players = {}
for elem in doc.findall( ".//diceEvent" ):
player_name = elem.attrib[ "player" ]
if player_name not in players:
# NOTE: ChartJS (in the frontend Javascript) identifies datasets using a 0-based index,
# so to avoid accidentally mixing these up with player ID's, we generate non-numeric player ID's.
player_id = "p:{}".format( len(players) + 1 )
players[ player_name ] = player_id
# generate the results for each log file
log_files = []
for logFileElem in doc.findall( ".//logFile" ):
# process the events for the next log file
events, scenario = [], {}
for elem in logFileElem.find( ".//events" ):
if elem.tag == "diceEvent":
# found a DICE ROLL event
player_id = players[ elem.attrib["player"] ]
values = [ int(v) for v in elem.text.split(",") ]
events.append( {
"eventType": "roll",
"playerId": player_id,
"rollType": elem.attrib[ "rollType" ],
"rollValue": values[0] if len(values) == 1 else values
} )
elif elem.tag == "turnTrackEvent":
# found a TURN TRACK event
events.append( {
"eventType": "turnTrack",
"side": elem.attrib[ "side" ],
"turnNo": elem.attrib[ "turnNo" ],
"phase": elem.attrib[ "phase" ]
} )
else:
if logger:
logger.warn( "Found an unknown analysis event: %s", elem.tag )
# extract the scenario details
elem = logFileElem.find( ".//scenario" )
if elem is not None:
scenario[ "scenarioName" ] = elem.text
if "id" in elem.attrib:
scenario[ "scenarioId" ] = elem.attrib["id"]
log_files.append( {
"filename": logFileElem.attrib[ "filename" ],
"scenario": scenario,
"events": events,
} )
return {
"players": { v: k for k,v in players.items() },
"logFiles": log_files
}

@ -12,6 +12,7 @@ from vasl_templates.webapp.utils import MsgStore
import vasl_templates.webapp.config.constants
from vasl_templates.webapp.config.constants import BASE_DIR, DATA_DIR
from vasl_templates.webapp import globvars
from vasl_templates.webapp.lfa import DEFAULT_LFA_DICE_HOTNESS_WEIGHTS, DEFAULT_LFA_DICE_HOTNESS_THRESHOLDS
# NOTE: This is used to stop multiple instances of the program from running (see main.py in the desktop app).
INSTANCE_ID = uuid.uuid4().hex
@ -72,6 +73,17 @@ _APP_CONFIG_DEFAULTS = { # Bodhgaya, India (APR/19)
def get_app_config():
"""Get the application config."""
def get_json_val( key, default ):
"""Get a JSON value from the app config."""
try:
val = app.config.get( key, default )
return val if isinstance(val,dict) else json.loads(val)
except json.decoder.JSONDecodeError:
msg = "Couldn't parse app config setting: {}".format( key )
logging.error( "%s", msg )
startup_msg_store.error( msg )
return default
# include the basic app config
vals = {
key: app.config.get( key, default )
@ -80,6 +92,15 @@ def get_app_config():
for key in ["APP_NAME","APP_VERSION","APP_DESCRIPTION","APP_HOME_URL"]:
vals[ key ] = getattr( vasl_templates.webapp.config.constants, key )
# include the dice hotness config
vals[ "LFA_DICE_HOTNESS_WEIGHTS" ] = get_json_val(
"LFA_DICE_HOTNESS_WEIGHTS", DEFAULT_LFA_DICE_HOTNESS_WEIGHTS
)
vals[ "LFA_DICE_HOTNESS_THRESHOLDS" ] = get_json_val(
"LFA_DICE_HOTNESS_THRESHOLDS", DEFAULT_LFA_DICE_HOTNESS_THRESHOLDS
)
vals[ "DISABLE_LFA_HOTNESS_FADEIN" ] = app.config.get( "DISABLE_LFA_HOTNESS_FADEIN" )
# NOTE: We allow the front-end to generate snippets that point to an alternative webapp server (so that
# VASSAL can get images, etc. from a Docker container rather than the desktop app). However, since it's
# unlikely anyone else will want this option, we implement it as a debug setting, rather than exposing it

@ -0,0 +1,284 @@
/* jshint esnext: true */
// --------------------------------------------------------------------
// Wrapper around the results of a log file analysis.
class LogFileAnalysis
{
constructor( data, logFileNo ) {
// initialize
var logFiles=[], playersSeen={}, events=[], title=null, title2=null ;
// process each log file
data.logFiles.forEach( function( logFile, index ) {
if ( logFileNo >= 0 && logFileNo != index )
return ;
// record the next log file and generate an event for it
logFiles.push( logFile.filename ? logFile.filename : "file #"+(1+index) ) ;
events.push( { eventType: "logFile", filename: logFile.filename } ) ;
// take a copy of each event
logFile.events.forEach( function( evt ) {
events.push( evt ) ;
playersSeen[ evt.playerId ] = true ;
} ) ;
// check if we found a scenario name in the log file
if ( logFile.scenario.scenarioName ) {
// NOTE: We prefer the last (i.e. probably the most recent) scenario name/ID.
title = logFile.scenario.scenarioName ;
title2 = logFile.scenario.scenarioId ;
}
} ) ;
// check if we found a scenario name
if ( ! title ) {
// nope - just use the log filename
title = logFiles[0] ;
if ( logFiles.length > 1 )
title2 = "and " + (logFiles.length-1) + " " + pluralString(logFiles.length-1,"other","others") ;
}
// keep only the players seen in the events
var players={}, playerIds=[] ;
for ( var playerId in data.players ) {
if ( ! playersSeen[playerId] )
continue ;
players[ playerId ] = data.players[ playerId ] ;
playerIds.push( playerId ) ;
}
// check if the user is one of the scenario players
if ( gUserSettings[ "vasl-username" ] ) {
var vaslUserName = gUserSettings[ "vasl-username" ].toLowerCase() ;
for ( var i=0 ; i < playerIds.length ; ++i ) {
playerId = playerIds[ i ] ;
if ( players[ playerId ].toLowerCase() === vaslUserName ) {
// yup - change their name to "Me" and put them first
players[ playerId ] = "Me" ;
var tmp = playerIds[0] ;
playerIds[0] = playerId ;
playerIds[i] = tmp ;
break ;
}
}
}
// save the extracted results
this._players = players ; // nb: maps player ID's to names
this._playerIds = playerIds ; // nb: ordered list of player ID's
this.logFiles = logFiles ;
this.logFileNo = logFileNo ;
this.events = events ;
this.title = title ;
this.title2 = title2 ;
}
extractStats( filter ) {
// initialize
var events = this.events ;
function doExtractStats( playerId, singleDie ) {
// initialize
var stats2 = { nRolls: 0, distrib: {} } ;
var rollTotal = 0 ;
// process each event
events.forEach( function( evt ) {
if ( evt.eventType !== "roll" || playerId != evt.playerId )
return ;
if ( filter && ! filter(evt) )
return ;
if ( singleDie && ! LogFileAnalysis.isSingleDie( evt.rollValue ) )
return ;
else if ( ! singleDie && LogFileAnalysis.isSingleDie( evt.rollValue ) )
return ;
stats2.nRolls += 1 ;
var evtRollTotal = LogFileAnalysis.rollTotal( evt.rollValue ) ;
rollTotal += evtRollTotal ;
if ( stats2.distrib[ evtRollTotal ] )
stats2.distrib[ evtRollTotal ] += 1 ;
else
stats2.distrib[ evtRollTotal ] = 1 ;
} ) ;
stats2.rollAverage = rollTotal / stats2.nRolls ;
return stats2 ;
}
// extract the stats for each player
var stats = { totalRolls: { DR: 0, dr: 0 } } ;
this.forEachPlayer( function( playerId ) {
stats[ playerId ] = {
DR: doExtractStats( playerId, false ),
dr: doExtractStats( playerId, true ),
} ;
stats.totalRolls.DR += stats[playerId].DR.nRolls ;
stats.totalRolls.dr += stats[playerId].dr.nRolls ;
} ) ;
return stats ;
}
extractEvents( windowSize, handlers ) {
// initialize
var windowVals={}, events=[], nRolls={} ;
this.forEachPlayer( function( playerId ) {
windowVals[playerId] = [] ;
nRolls[playerId] = 0 ;
} ) ;
function callHandler( handlerName, evt ) {
// invoke the specified handler
if ( handlerName[0] != "_" )
handlerName = "on" + handlerName[0].toUpperCase() + handlerName.substring(1) + "Event" ;
var handler = handlers[ handlerName ] ;
if ( ! handler )
return null ;
return handler( evt ) ;
}
// process each event
this.events.forEach( function( evt ) {
// notify the caller
var rc = callHandler( evt.eventType, evt ) ;
if ( rc === false )
return ; // nb: the caller wants to ignore this event
// check if this is a DR/dr roll
if ( evt.eventType === "roll" ) {
// yup - update the values in the window buffer
var playerId = evt.playerId ;
++ nRolls[ playerId ] ;
windowVals[playerId].push( evt.rollValue ) ;
if ( windowVals[playerId].length < windowSize ) {
return ;
}
// calculate the next moving average from the buffered values
var rollTotal = windowVals[playerId].reduce( function( total, v ) {
return total + LogFileAnalysis.rollTotal( v ) ;
}, 0 ) ;
var movingAverage = rollTotal / windowVals[playerId].length ;
windowVals[playerId].shift() ;
var newEvent = {
eventType: evt.eventType,
playerId: playerId,
rollType: evt.rollType,
rollValue: evt.rollValue,
movingAverage: movingAverage,
rollNo: nRolls[ playerId ],
} ;
// add the new value to the results
callHandler( "_onAddEvent", newEvent ) ;
events.push( newEvent ) ;
}
} ) ;
return {
events: events,
nRolls: nRolls,
windowSize: windowSize
} ;
}
calcHotness( stats ) {
// Dice "hotness" is a metric that tries to capture how good a set of rolls are. Chi-squared is the metric
// usually used to determine how far a set of observed values is from the expected distribution, but it doesn't
// distinguish from the rolls tending towards high or low values.
//
// So, we modify the chi-squared calculation as follows:
// - take the square of the difference between the observed and expected values (as normal)
// - however, we preserve the sign, then multiply it by a weight
// - the values are summed
//
// These changes have the following effect:
// - Weighting the columns means that if we roll more than the expected number of 5's and 6's, that will
// increase the score, but it we roll more 2's and 3's, that will increase the score by even more.
// - Also, because the weights are negative for rolls >= 8, rolling more of these makes the score go down.
// - Preserving the sign means that if we roll more 2's than expected, the score increases, but if we roll
// fewer than expected, the score will decrease. Similarly, if we roll more 10's than expected, the score
// will go down (because while the squared difference is positive, the weight is negative).
function doCalcHotness( stats, expected, weights ) {
if ( stats.nRolls === 0 )
return null ;
var total = 0 ;
for ( var val in weights ) {
var observed = stats.distrib[val] || 0 ;
var diff = observed/stats.nRolls - expected[val]/100 ;
var sign = diff < 0 ? -1 : +1 ;
var delta = sign * Math.pow(diff,2) * weights[val] / (expected[val]/100) ;
total += delta ;
}
return total ;
}
// calculate how hot the dice were
var results = {} ;
this.forEachPlayer( function( playerId ) {
var hotness = {} ;
for ( var key in LogFileAnalysis.EXPECTED_DISTRIB ) {
hotness[ key ] = doCalcHotness(
stats[ playerId ][ key ],
LogFileAnalysis.EXPECTED_DISTRIB[ key ],
gAppConfig.LFA_DICE_HOTNESS_WEIGHTS[ key ]
) ;
}
// NOTE: Dice hotness (and chi-squared) aren't particularly meaningful for small datasets,
// and can have very skewed results. However, if there are enough DR's, we don't want to let
// there only being a few dr's from stopping us from showing a result. It's tricky to handle
// this case (we can't just add in the dr score if there are enough dr's, since that would
// benefit someone over another player who didn't have enough dr's), so we just ignore dr's :-/
var rollRatio = stats[playerId].DR.nRolls / gAppConfig.LFA_DICE_HOTNESS_THRESHOLDS.DR ;
results[ playerId ] = [ hotness.DR, rollRatio ] ;
} ) ;
return results ;
}
getRollTypes() {
// return the roll types
var rollTypes = {} ;
this.events.forEach( function( evt ) {
if ( evt.eventType === "roll" && ! rollTypes[ evt.rollType ] )
rollTypes[ evt.rollType ] = true ;
} );
return Object.keys( rollTypes ) ;
}
forEachPlayer( func ) {
// call the specified function for each player
var playerIds = this.playerIds() ;
for ( var i=0 ; i < playerIds.length ; ++i )
func( playerIds[i], i ) ;
}
playerIds() { return this._playerIds ; }
playerName( playerId ) { return this._players[ playerId ] ; }
static rollTotal( roll ) {
// return the total of a DR/dr
if ( LogFileAnalysis.isSingleDie( roll ) )
return roll ;
else
return roll.reduce( function (total,n) { return total + n ; }, 0 ) ;
}
static isSingleDie( roll ) {
// check if a roll is a DR or dr
return ! Array.isArray( roll ) ;
}
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
LogFileAnalysis.EXPECTED_DISTRIB = {
DR: { 2: 2.8, 3: 5.6, 4: 8.3, 5: 11.1, 6: 13.9, 7: 16.7, 8: 13.9, 9: 11.1, 10: 8.3, 11: 5.6, 12: 2.8 },
dr: { 1: 16.7, 2: 16.7, 3: 16.7, 4: 16.7, 5: 16.7, 6: 16.7 },
} ;

@ -0,0 +1 @@
@keyframes chartjs-render-animation{from{opacity:.99}to{opacity:1}}.chartjs-render-monitor{animation:chartjs-render-animation 1ms}.chartjs-size-monitor,.chartjs-size-monitor-expand,.chartjs-size-monitor-shrink{position:absolute;direction:ltr;left:0;top:0;right:0;bottom:0;overflow:hidden;pointer-events:none;visibility:hidden;z-index:-1}.chartjs-size-monitor-expand>div{position:absolute;width:1000000px;height:1000000px;left:0;top:0}.chartjs-size-monitor-shrink>div{position:absolute;width:200%;height:200%;left:0;top:0}

File diff suppressed because one or more lines are too long

@ -0,0 +1,25 @@
/**
* [chartjs-plugin-labels]{@link https://github.com/emn178/chartjs-plugin-labels}
*
* @version 1.1.0
* @author Chen, Yi-Cyuan [emn178@gmail.com]
* @copyright Chen, Yi-Cyuan 2017-2018
* @license MIT
*/
(function(){function f(){this.renderToDataset=this.renderToDataset.bind(this)}if("undefined"===typeof Chart)console.error("Can not find Chart object.");else{"function"!=typeof Object.assign&&(Object.assign=function(a,c){if(null==a)throw new TypeError("Cannot convert undefined or null to object");for(var b=Object(a),e=1;e<arguments.length;e++){var d=arguments[e];if(null!=d)for(var g in d)Object.prototype.hasOwnProperty.call(d,g)&&(b[g]=d[g])}return b});var k={};["pie","doughnut","polarArea","bar"].forEach(function(a){k[a]=
!0});f.prototype.setup=function(a,c){this.chart=a;this.ctx=a.ctx;this.args={};this.barTotal={};var b=a.config.options;this.options=Object.assign({position:"default",precision:0,fontSize:b.defaultFontSize,fontColor:b.defaultFontColor,fontStyle:b.defaultFontStyle,fontFamily:b.defaultFontFamily,shadowOffsetX:3,shadowOffsetY:3,shadowColor:"rgba(0,0,0,0.3)",shadowBlur:6,images:[],outsidePadding:2,textMargin:2,overlap:!0},c);"bar"===a.config.type&&(this.options.position="default",this.options.arc=!1,this.options.overlap=
!0)};f.prototype.render=function(){this.labelBounds=[];this.chart.data.datasets.forEach(this.renderToDataset)};f.prototype.renderToDataset=function(a,c){this.totalPercentage=0;this.total=null;var b=this.args[c];b.meta.data.forEach(function(c,d){this.renderToElement(a,b,c,d)}.bind(this))};f.prototype.renderToElement=function(a,c,b,e){if(this.shouldRenderToElement(c.meta,b)&&(this.percentage=null,c=this.getLabel(a,b,e))){var d=this.ctx;d.save();d.font=Chart.helpers.fontString(this.options.fontSize,
this.options.fontStyle,this.options.fontFamily);var g=this.getRenderInfo(b,c);this.drawable(b,c,g)&&(d.beginPath(),d.fillStyle=this.getFontColor(a,b,e),this.renderLabel(c,g));d.restore()}};f.prototype.renderLabel=function(a,c){return this.options.arc?this.renderArcLabel(a,c):this.renderBaseLabel(a,c)};f.prototype.renderBaseLabel=function(a,c){var b=this.ctx;if("object"===typeof a)b.drawImage(a,c.x-a.width/2,c.y-a.height/2,a.width,a.height);else{b.save();b.textBaseline="top";b.textAlign="center";this.options.textShadow&&
(b.shadowOffsetX=this.options.shadowOffsetX,b.shadowOffsetY=this.options.shadowOffsetY,b.shadowColor=this.options.shadowColor,b.shadowBlur=this.options.shadowBlur);for(var e=a.split("\n"),d=0;d<e.length;d++)b.fillText(e[d],c.x,c.y-this.options.fontSize/2*e.length+this.options.fontSize*d);b.restore()}};f.prototype.renderArcLabel=function(a,c){var b=this.ctx,e=c.radius,d=c.view;b.save();b.translate(d.x,d.y);if("string"===typeof a){b.rotate(c.startAngle);b.textBaseline="middle";b.textAlign="left";d=
a.split("\n");var g=0,l=[],f=0;"border"===this.options.position&&(f=(d.length-1)*this.options.fontSize/2);for(var h=0;h<d.length;++h){var m=b.measureText(d[h]);m.width>g&&(g=m.width);l.push(m.width)}for(h=0;h<d.length;++h){var n=d[h],k=(d.length-1-h)*-this.options.fontSize+f;b.save();b.rotate((g-l[h])/2/e);for(var p=0;p<n.length;p++){var q=n.charAt(p);m=b.measureText(q);b.save();b.translate(0,-1*e);b.fillText(q,0,k);b.restore();b.rotate(m.width/e)}b.restore()}}else b.rotate((d.startAngle+Math.PI/
2+c.endAngle)/2),b.translate(0,-1*e),this.renderLabel(a,{x:0,y:0});b.restore()};f.prototype.shouldRenderToElement=function(a,c){return!a.hidden&&!c.hidden&&(this.options.showZero||"polarArea"===this.chart.config.type?0!==c._view.outerRadius:0!==c._view.circumference)};f.prototype.getLabel=function(a,c,b){if("function"===typeof this.options.render)a=this.options.render({label:this.chart.config.data.labels[b],value:a.data[b],percentage:this.getPercentage(a,c,b),dataset:a,index:b});else switch(this.options.render){case "value":a=
a.data[b];break;case "label":a=this.chart.config.data.labels[b];break;case "image":a=this.options.images[b]?this.loadImage(this.options.images[b]):"";break;default:a=this.getPercentage(a,c,b)+"%"}"object"===typeof a?a=this.loadImage(a):null!==a&&void 0!==a&&(a=a.toString());return a};f.prototype.getFontColor=function(a,c,b){var e=this.options.fontColor;"function"===typeof e?e=e({label:this.chart.config.data.labels[b],value:a.data[b],percentage:this.getPercentage(a,c,b),backgroundColor:a.backgroundColor[b],
dataset:a,index:b}):"string"!==typeof e&&(e=e[b]||this.chart.config.options.defaultFontColor);return e};f.prototype.getPercentage=function(a,c,b){if(null!==this.percentage)return this.percentage;if("polarArea"===this.chart.config.type){if(null===this.total)for(c=this.total=0;c<a.data.length;++c)this.total+=a.data[c];a=a.data[b]/this.total*100}else if("bar"===this.chart.config.type){if(void 0===this.barTotal[b])for(c=this.barTotal[b]=0;c<this.chart.data.datasets.length;++c)this.barTotal[b]+=this.chart.data.datasets[c].data[b];
a=a.data[b]/this.barTotal[b]*100}else a=c._view.circumference/this.chart.config.options.circumference*100;a=parseFloat(a.toFixed(this.options.precision));this.options.showActualPercentages||("bar"===this.chart.config.type&&(this.totalPercentage=this.barTotalPercentage[b]||0),this.totalPercentage+=a,100<this.totalPercentage&&(a-=this.totalPercentage-100,a=parseFloat(a.toFixed(this.options.precision))),"bar"===this.chart.config.type&&(this.barTotalPercentage[b]=this.totalPercentage));return this.percentage=
a};f.prototype.getRenderInfo=function(a,c){return"bar"===this.chart.config.type?this.getBarRenderInfo(a,c):this.options.arc?this.getArcRenderInfo(a,c):this.getBaseRenderInfo(a,c)};f.prototype.getBaseRenderInfo=function(a,c){if("outside"===this.options.position||"border"===this.options.position){var b,e=a._view,d=e.startAngle+(e.endAngle-e.startAngle)/2,g=e.outerRadius/2;"border"===this.options.position?b=(e.outerRadius-g)/2+g:"outside"===this.options.position&&(b=e.outerRadius-g+g+this.options.textMargin);
b={x:e.x+Math.cos(d)*b,y:e.y+Math.sin(d)*b};"outside"===this.options.position&&(d=this.options.textMargin+this.measureLabel(c).width/2,b.x+=b.x<e.x?-d:d);return b}return a.tooltipPosition()};f.prototype.getArcRenderInfo=function(a,c){var b=a._view;var e="outside"===this.options.position?b.outerRadius+this.options.fontSize+this.options.textMargin:"border"===this.options.position?(b.outerRadius/2+b.outerRadius)/2:(b.innerRadius+b.outerRadius)/2;var d=b.startAngle,g=b.endAngle,l=g-d;d+=Math.PI/2;g+=
Math.PI/2;var f=this.measureLabel(c);d+=(g-(f.width/e+d))/2;return{radius:e,startAngle:d,endAngle:g,totalAngle:l,view:b}};f.prototype.getBarRenderInfo=function(a,c){var b=a.tooltipPosition();b.y-=this.measureLabel(c).height/2+this.options.textMargin;return b};f.prototype.drawable=function(a,c,b){if(this.options.overlap)return!0;if(this.options.arc)return b.endAngle-b.startAngle<=b.totalAngle;var e=this.measureLabel(c);c=b.x-e.width/2;var d=b.x+e.width/2,g=b.y-e.height/2;b=b.y+e.height/2;return"outside"===
this.options.renderInfo?this.outsideInRange(c,d,g,b):a.inRange(c,g)&&a.inRange(c,b)&&a.inRange(d,g)&&a.inRange(d,b)};f.prototype.outsideInRange=function(a,c,b,e){for(var d=this.labelBounds,g=0;g<d.length;++g){for(var f=d[g],k=[[a,b],[a,e],[c,b],[c,e]],h=0;h<k.length;++h){var m=k[h][0],n=k[h][1];if(m>=f.left&&m<=f.right&&n>=f.top&&n<=f.bottom)return!1}k=[[f.left,f.top],[f.left,f.bottom],[f.right,f.top],[f.right,f.bottom]];for(h=0;h<k.length;++h)if(m=k[h][0],n=k[h][1],m>=a&&m<=c&&n>=b&&n<=e)return!1}d.push({left:a,
right:c,top:b,bottom:e});return!0};f.prototype.measureLabel=function(a){if("object"===typeof a)return{width:a.width,height:a.height};var c=0;a=a.split("\n");for(var b=0;b<a.length;++b){var e=this.ctx.measureText(a[b]);e.width>c&&(c=e.width)}return{width:c,height:this.options.fontSize*a.length}};f.prototype.loadImage=function(a){var c=new Image;c.src=a.src;c.width=a.width;c.height=a.height;return c};Chart.plugins.register({id:"labels",beforeDatasetsUpdate:function(a,c){if(k[a.config.type]){Array.isArray(c)||
(c=[c]);var b=c.length;a._labels&&b===a._labels.length||(a._labels=c.map(function(){return new f}));for(var e=!1,d=0,g=0;g<b;++g){var l=a._labels[g];l.setup(a,c[g]);"outside"===l.options.position&&(e=!0,l=1.5*l.options.fontSize+l.options.outsidePadding,l>d&&(d=l))}e&&(a.chartArea.top+=d,a.chartArea.bottom-=d)}},afterDatasetUpdate:function(a,c,b){k[a.config.type]&&a._labels.forEach(function(a){a.args[c.index]=c})},beforeDraw:function(a){k[a.config.type]&&a._labels.forEach(function(a){a.barTotalPercentage=
{}})},afterDatasetsDraw:function(a){k[a.config.type]&&a._labels.forEach(function(a){a.render()})}})}})();

@ -0,0 +1,37 @@
.ui-dialog.lfa-upload .ui-dialog-titlebar { background: #e0c090 ; }
.ui-dialog.lfa-upload .ui-dialog-buttonpane { border: none ; margin-top: 0 !important ; padding-top: 0 !important ; }
.ui-dialog.lfa-upload .ui-dialog-buttonpane button.add { display: inline-flex ; align-items: center ; margin-left: 0 !important ; height: 25px ; padding: 2px 8px 2px 6px ; }
.ui-dialog.lfa-upload .ui-dialog-buttonpane button.add img { height: 15px ; margin-right: 0.5em ; }
#lfa-upload {
overflow-x: hidden ; overflow-y: hidden ;
}
#lfa-upload .files {
width: 100% ; height: 99% ;
margin: 0 ;
border: 1px solid #ccc ; border-radius: 2px ;
overflow-x: hidden ;
}
#lfa-upload .files li {
display: flex ;
margin: 2px ;
border: 1px solid #ccc ; border-radius: 2px ;
background: #f0e0c0 ;
padding: 8px 5px 2px 8px ;
list-style-type: none ;
cursor: pointer ;
}
#lfa-upload .files li img.file { height: 1.5em ; margin-right: 0.5em ; }
#lfa-upload .files li .filename { flex: 1 ; height: 1.5em ; padding: 4px 3px ; }
#lfa-upload .files li.dragOutside { background: #f0d0d0 ; text-decoration: line-through ; }
#lfa-upload .files li .delete { float: right ; height: 10px ; margin: -2px ; }
#lfa-upload .hint {
position: absolute ; top: 20px ; left: 20px ; bottom: 20px ; right: 20px ;
display: flex ; flex-direction: column ; justify-content: center ;
font-style: italic ; color: #666 ;
z-index: 1 ;
}
#lfa-upload .hint .content { display: flex ; justify-content: center ; }
#lfa-upload .hint .content img { float: left ; margin-right: 0.75em ; opacity: 0.6 }

@ -0,0 +1,113 @@
/* jQuery dialog */
.ui-dialog.lfa {
width: calc(100% - 10px) !important ;
height: calc(100% - 10px) !important ;
left: 5px ;
top: 5px ;
border-radius: 5px ;
}
.ui-dialog.lfa .ui-dialog-titlebar { display: none ; }
#lfa {
height: 100% !important ;
display: flex ; flex-direction: column ;
overflow-y: auto ;
/* style the HTML elements to match the charts */
font-size: 80% ; font-family: 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif ; color: #333 ;
}
#lfa ::selection {}
#lfa ::-moz-selection {}
#lfa { user-select: none ; }
/* splitter */
#lfa .split.top-pane { display: flex ; min-height: 350px ; }
#lfa .split.top-pane .right { position: relative ; }
#lfa .split.bottom-pane { min-height: 200px ; position: relative ; }
#lfa .gutter { position: relative ; min-height: 3px ; background-color: #f0f0f0 ; cursor: row-resize ; z-index: 49 ; }
#lfa .gutter img { position: absolute ; left: 40% ; top: -2px ; }
/* top pane: dr/DR distribution & pie charts */
#lfa .distrib { position: relative ; }
#lfa .pie { display: inline-block ; width: 200px ; height: 90px ; }
#lfa .pie canvas { margin: -20px 0 0 -40px ; }
#lfa .hotness { position: absolute ; bottom: 20px ; right: 5px ; }
#lfa .banner { position: relative ; display: inline-block ; }
#lfa .options { position: absolute ; top: 0 ; right: 25px ; }
/* bottom pane: time-plot chart */
#lfa .time-plot-options {
display: inline-block ; position: absolute ; top: -14px ; right: 0 ;
z-index: 50 ;
padding-left: 0.5em ;
background: white ;
}
#lfa .time-plot { overflow-x: auto ; }
/* banner */
#lfa .banner {
min-width: 20em ;
margin: 0 1em 0.5em 0 ;
border: 1px solid #aaa ; border-radius: 5px ;
padding: 0.5em 1em ;
background: #ffffe0 ; color: #444 ;
}
#lfa .banner .title { font-size: 150% ; }
#lfa .banner .title2 { font-style: italic ; }
#lfa .banner .roll-type { font-size: 110% ; font-style: italic ; color: #666 ; }
#lfa .banner .select-file { position: absolute ; bottom: 2px ; right: 2px ; font-style: italic ; color: #444 ; cursor: pointer ; }
#lfa .banner .select-file .caption { line-height: 18px ; vertical-align: middle ; margin-right: 0.15em ; }
/* dice hotness */
#lfa .hotness {
width: 300px ;
border: 1px solid #ffc030 ; border-radius: 5px ; background: #fffcfc ;
padding: 2px 5px ;
}
#lfa .hotness img.dice { position: absolute ; left: -10px ; top : -25px ; height: 50px ; }
/* options panel */
#lfa .options {
border: 1px solid #ccc ; border-radius: 5px ; padding: 0.5em ;
background: #fffff8 ;
}
#lfa .options input[type=checkbox] { vertical-align: middle ; }
#lfa .options button.download { position: absolute ; right: 5px ; bottom: 5px ; padding: 3px !important ; }
#lfa .options button.player-colors { padding: 3px 6px 2px 5px !important ; }
/* time-plot options */
#lfa .time-plot-options button.zoom-in, #lfa .time-plot-options button.zoom-out { padding: 4px 6px 2px 6px !important ; }
/* player colors popup */
#lfa .player-colors-popup, #lfa .select-file-popup {
border: 1px solid #aaa ; border-radius: 5px ;
background: #f8f8f8 ;
z-index: 100 ;
}
#lfa .player-colors-popup .row { margin: 0.5em ; padding-top: 3px ; }
#lfa .player-colors-popup .sp-replacer { width: 40px ; height: 17px ; margin: -3px 0.5em 0 0 ; }
#lfa .player-colors-popup .sp-preview { width: 20px ; height: 15px ; }
/* file selection popup */
#lfa .select-file-popup .row { margin: 6px 8px ; }
#lfa .select-file-popup input[type="radio"] { margin: -2px 0.5em 0 0 ; vertical-align: middle ; }
/* "no data" marker */
#lfa .no-data {
width: 5em ;
border: 1px dotted #aaa ; border-radius: 5px ;
padding: 0.15em 0.25em 0.1em 0.25em ;
background: rgba(128,128,128,0.1) ;
color: #888 ; font-size: 150% ; text-align: center ;
}
/* tabular chart data (for testing porpoises) */
#lfa table.chart-data { border: 1px solid #888 ; font-size: 80% ; }
#lfa table.chart-data th { padding: 2px 5px ; border-bottom: 1px dotted #888 ; }
#lfa table.chart-data td { padding: 2px 5px ; text-align: center ; }
#lfa table.chart-data td.label { border-right: 1px dotted #888 ; min-width: 1em ; }
/* miscellaneous */
#lfa button.ui-dialog-titlebar-close { position: fixed ; top: 15px ; right: 5px ; }
#lfa .ui-selectmenu-button { padding: 2px 5px ; }
#lfa input[type="checkbox"] { margin-bottom: 0.2em ; }
#lfa label[disabled] { color: #aaa ; }

@ -2,6 +2,7 @@
.ui-dialog.user-settings .ui-dialog-buttonpane { border: none ; margin-top: 0 !important ; padding-top: 0 !important ; }
.ui-dialog.user-settings .row { height: 1.5em ; }
.ui-dialog.user-settings .row label { height: 20px ; margin-top: 2px ; }
.ui-dialog.user-settings fieldset { margin: 0.5em 0 0 0 ; padding-top: 0.5em ; border-radius: 0 ; }
.ui-dialog.user-settings .select2 { margin-top: -4px ; }

Binary file not shown.

After

Width:  |  Height:  |  Size: 139 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 810 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 938 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

@ -0,0 +1,26 @@
/* jshint esnext: true */
// Manage jQuery event handlers.
class jQueryHandlers
{
constructor() {
// initialize
this.events = [] ;
}
addHandler( $elem, evtType, handler ) {
// add an event handler
$elem.on( evtType, handler ) ;
this.events.push( [ $elem, evtType, handler ] ) ;
}
cleanUp() {
// clean up event handlers
for ( var i=this.events.length-1 ; i >= 0 ; --i ) {
var evt = this.events[ i ] ;
evt[0].off( evt[1], evt[2] ) ;
}
}
}

@ -0,0 +1,288 @@
( function() { // nb: put the entire file into its own local namespace, global stuff gets added to window.
var $gLogFilesToUpload ;
var gEventHandlers ;
var gDlgSizeAndPosition = {} ;
var gDisableClickToAddTimestamp = new Date() ;
// --------------------------------------------------------------------
window.on_analyze_vlog = function()
{
// initialize
var $dlg ;
function onAddFile() {
// FUDGE! Files can be removed from the upload list by using the mouse (e.g. Ctrl-Click,
// or clicking on the "delete" icon), so we don't want to also trigger an "add" dialog.
var delta = (new Date()).getTime() - gDisableClickToAddTimestamp.getTime() ;
if ( delta <= 5 )
return ;
// add a file to the list of files to be analyzed
if ( getUrlParam( "vlog_persistence" ) ) {
// 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>).
var $elem = $( "#_vlog-persistence_" ) ;
var data = $elem.val() ;
var pos = data.indexOf( "|" ) ;
var fname = data.substring( 0, pos ) ;
var vlog_data = data.substring( pos+1 ) ;
$elem.val( "" ) ; // nb: let the test suite know we've received the data
addFileToUploadList( fname, vlog_data ) ;
} else {
$("#load-vlog").trigger( "click" ) ; // nb: will call on_load_vlog_file_selected() when done
}
}
// handle drag events for items already in the upload list
var isDraggedOutside = null ;
function onSortStart( evt, ui ) {
isDraggedOutside = false ;
}
function onDragOutside( evt, ui ) { // nb: we get one of these even after a drag has ended :-/
if ( isDraggedOutside === null )
return ;
isDraggedOutside = true ;
ui.item.addClass( "dragOutside" ) ;
}
function onDragInside( evt, ui ) {
isDraggedOutside = false ;
ui.item.removeClass( "dragOutside" ) ;
}
function onDragEnd( evt, ui ) {
if ( isDraggedOutside )
removeFileFromUploadList( ui.item ) ;
isDraggedOutside = null ;
}
// handle events for files being dragged in from outside the browser
function initExternalDragDrop() {
[ $gLogFilesToUpload, $dlg.find(".hint") ].forEach( function( $elem ) {
gEventHandlers.addHandler( $elem, "dragenter", stopEvent ) ;
gEventHandlers.addHandler( $elem, "dragleave", stopEvent ) ;
gEventHandlers.addHandler( $elem, "dragover", stopEvent ) ;
gEventHandlers.addHandler( $elem, "drop", function( evt ) {
// add the files dragged in to the upload list
addFilesToUploadList( evt.originalEvent.dataTransfer.files ) ;
stopEvent( evt ) ;
} ) ;
} ) ;
}
// NOTE: We can't use the normal mechanism for handling Ctrl-Enter, since there are no input elements.
// We do things using a document-level keydown event handler.
function onKeyDown( evt ) {
if ( $gLogFilesToUpload.find( "li" ).length === 0 ) {
evt.preventDefault() ;
return false ;
}
auto_dismiss_dialog( $dlg, evt, "OK" ) ;
}
// show the dialog
gEventHandlers = new jQueryHandlers() ;
$( "#lfa-upload" ).dialog( {
title: "Analyze log files",
dialogClass: "lfa-upload",
modal: true,
width: Math.min( gDlgSizeAndPosition.width || 400, $(window).innerWidth() ),
minWidth: 400,
height: Math.min( gDlgSizeAndPosition.height || 300, $(window).innerHeight() ),
minHeight: 300,
position: { my: "center", at: "center", of: window },
create: function() {
// initialize the dialog
init_dialog( $(this), "OK", false ) ;
var $btnPane = $( ".ui-dialog.lfa-upload .ui-dialog-buttonpane" ) ;
var $btn = $( "<button class='add'> <img src='" + gImagesBaseUrl+"/sortable-add.png'> Add </button>" ) ;
$btnPane.prepend( $btn ) ;
},
open: function() {
// initialize the dialog
$dlg = $(this) ;
on_dialog_open( $(this) ) ;
gEventHandlers.addHandler( $(document), "keydown", onKeyDown ) ;
$gLogFilesToUpload = $( "#lfa-upload .files" ) ;
$gLogFilesToUpload.sortable( {
start: onSortStart,
out: onDragOutside,
over: onDragInside,
beforeStop: onDragEnd,
} ).empty() ;
initExternalDragDrop() ;
var $addBtn = $( ".ui-dialog.lfa-upload button.add" ) ;
gEventHandlers.addHandler( $addBtn, "click", onAddFile ) ;
gEventHandlers.addHandler( $gLogFilesToUpload, "click", onAddFile ) ;
gEventHandlers.addHandler( $dlg.find(".hint"), "click", onAddFile ) ;
updateUi() ;
},
beforeClose: function() {
// save the current size and position
gDlgSizeAndPosition = getElemSizeAndPosition( $(".ui-dialog.lfa-upload") ) ;
},
close: function() {
// clean up handlers
gEventHandlers.cleanUp() ;
},
buttons: {
OK: function() {
// unload the files to be analyzed
var vlog_data = [] ;
$gLogFilesToUpload.children( "li" ).each( function() {
vlog_data.push( [ $(this).attr("data-filename"), $(this).attr("data-vlog") ] ) ;
} ) ;
// analyze the log files
$(this).dialog( "close" ) ;
analyzeLogFiles( vlog_data ) ;
},
Cancel: function() { $(this).dialog( "close" ) ; },
},
} ) ;
} ;
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
window.on_load_vlog_file_selected = function() {
// add the selected files to the upload list
addFilesToUploadList( $( "#load-vlog" ).prop( "files" ) ) ;
} ;
function addFilesToUploadList( files )
{
// initialize
var currFileNo = 0 ;
var fileReader = new FileReader() ;
// add each log file to the list
function loadNextFile() {
if ( currFileNo >= files.length )
return ;
var currFile = files[ currFileNo ] ;
fileReader.onload = function() {
// get the file data
vlog_data = fileReader.result ;
if ( vlog_data.substring(0,5) === "data:" )
vlog_data = vlog_data.split( "," )[1] ;
// add the file to the list
addFileToUploadList( currFile.name, vlog_data ) ;
// read the next file
++ currFileNo ;
loadNextFile() ;
} ;
fileReader.readAsDataURL( currFile ) ;
}
loadNextFile() ;
}
function addFileToUploadList( fname, vlog_data )
{
// add the file to the upload list
var buf = [ "<li>",
"<img src='" + gImagesBaseUrl+"/lfa/file.png" + "' class='file'>",
"<span class='filename'>", fname, "</span>",
"<img src='" + gImagesBaseUrl+"/cross.png" + "' class='delete'>",
"</li>"
] ;
var $item = $( buf.join("") ) ;
$item.attr( "data-filename", fname ) ;
$item.attr( "data-vlog", vlog_data ) ;
$gLogFilesToUpload.append( $item ) ;
updateUi() ;
// add click handler to remove the file from the list
gEventHandlers.addHandler( $item.children( ".delete" ), "click", function() {
gDisableClickToAddTimestamp = new Date() ;
removeFileFromUploadList( $(this).parent() ) ;
} ) ;
gEventHandlers.addHandler( $item, "click", function( evt ) {
if ( evt.ctrlKey ) {
gDisableClickToAddTimestamp = new Date() ;
removeFileFromUploadList( $(this) ) ;
}
} ) ;
}
function removeFileFromUploadList( $item ) {
// remove the file from the upload list
$item.remove() ;
setTimeout( updateUi, 100 ) ; // nb: we need this after a drag-out :-/
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
function updateUi()
{
// update the UI
var nFiles = $gLogFilesToUpload.find( "li" ).length ;
var $btn = $( ".ui-dialog.lfa-upload button.ok" ) ;
var $hint = $( ".ui-dialog.lfa-upload .hint" ) ;
if ( nFiles > 0 ) {
$btn.button( "enable" ) ;
$hint.hide() ;
} else {
$btn.button( "disable" ) ;
$hint.show() ;
}
}
// --------------------------------------------------------------------
function analyzeLogFiles( vlog_data )
{
// send a request to analyze the log files
var objName = pluralString( vlog_data.length, "log file", "log files" ) ;
var $dlg = _show_vassal_shim_progress_dlg(
"Analyzing your " + objName + "..."
) ;
$.ajax( {
url: gAnalyzeVlogsUrl,
type: "POST",
data: JSON.stringify( vlog_data ),
contentType: "application/json",
} ).done( function( data ) {
$dlg.dialog( "close" ) ;
resp = checkResponse( data, objName ) ;
if ( ! resp )
return ;
show_lfa_dialog( resp ) ;
} ).fail( function( xhr, status, errorMsg ) {
$dlg.dialog( "close" ) ;
showErrorMsg( "Can't analyze the " + objName + ":<div class='pre'>" + escapeHTML(errorMsg) + "</div>" ) ;
} ) ;
}
function checkResponse( resp, objName )
{
// check if there was an error
if ( resp.error ) {
// yup - report it
if ( getUrlParam( "vlog_persistence" ) ) {
$( "#_vlog-persistence_" ).val(
"ERROR: " + resp.error + "\n\n=== STDOUT ===\n" + resp.stdout + "\n=== STDERR ===\n" + resp.stderr
) ;
} else {
show_vassal_shim_error_dlg( resp, "Can't analyze the " + objName + "." ) ;
}
return null ;
}
// check if anything was extracted
if ( resp.logFiles ) {
var totalEvents = 0 ;
resp.logFiles.forEach( function( logFile ) {
totalEvents += logFile.events.length ;
} ) ;
if ( totalEvents === 0 ) {
showWarningMsg( "Couldn't find anything in the " + objName + "." +
"<p> " + pluralString(resp.logFiles.length,"It's","They're") + " probably either not a log file, or from an old version of VASL."
) ;
return null ;
}
}
return resp ;
}
// --------------------------------------------------------------------
} )() ; // end local namespace

File diff suppressed because it is too large Load Diff

@ -56,6 +56,7 @@ $(document).ready( function () {
separator: { type: "separator" },
analyze_vsav: { label: "Analyze VASL scenario", action: on_analyze_vsav },
update_vsav: { label: "Update VASL scenario", action: on_update_vsav },
analyze_vlog: { label: "Analyze log files", action: on_analyze_vlog },
separator2: { type: "separator" },
template_pack: { label: "Load template pack", action: on_template_pack },
user_settings: { label: "Settings", action: user_settings },
@ -84,6 +85,7 @@ $(document).ready( function () {
$("#load-scenario").change( on_load_scenario_file_selected ) ;
$("#load-template-pack").change( on_template_pack_file_selected ) ;
$("#load-vsav").change( on_load_vsav_file_selected ) ;
$("#load-vlog").change( on_load_vlog_file_selected ) ;
// all done - we can show the menu now
$("#menu").show() ;
@ -387,6 +389,11 @@ $(document).ready( function () {
} ) ;
}
// prevent files from being dragged in
// NOTE: It would be nice to stop the cursor from changing, but there doesn't seem to be any way of doing that :-/
// In particualar, the dragstart events doesn't fire if something is being dragged into the browser from outside.
$(document).on( { dragenter: stopEvent, dragleave: stopEvent, dragover: stopEvent, drop: stopEvent } ) ;
// figure out how many pixels an em is
var $em = $( "<span>M</span>" ) ;
$("body").append( $em ) ;

@ -0,0 +1,507 @@
/***
Spectrum Colorpicker v1.8.1
https://github.com/bgrins/spectrum
Author: Brian Grinstead
License: MIT
***/
.sp-container {
position:absolute;
top:0;
left:0;
display:inline-block;
*display: inline;
*zoom: 1;
/* https://github.com/bgrins/spectrum/issues/40 */
z-index: 9999994;
overflow: hidden;
}
.sp-container.sp-flat {
position: relative;
}
/* Fix for * { box-sizing: border-box; } */
.sp-container,
.sp-container * {
-webkit-box-sizing: content-box;
-moz-box-sizing: content-box;
box-sizing: content-box;
}
/* http://ansciath.tumblr.com/post/7347495869/css-aspect-ratio */
.sp-top {
position:relative;
width: 100%;
display:inline-block;
}
.sp-top-inner {
position:absolute;
top:0;
left:0;
bottom:0;
right:0;
}
.sp-color {
position: absolute;
top:0;
left:0;
bottom:0;
right:20%;
}
.sp-hue {
position: absolute;
top:0;
right:0;
bottom:0;
left:84%;
height: 100%;
}
.sp-clear-enabled .sp-hue {
top:33px;
height: 77.5%;
}
.sp-fill {
padding-top: 80%;
}
.sp-sat, .sp-val {
position: absolute;
top:0;
left:0;
right:0;
bottom:0;
}
.sp-alpha-enabled .sp-top {
margin-bottom: 18px;
}
.sp-alpha-enabled .sp-alpha {
display: block;
}
.sp-alpha-handle {
position:absolute;
top:-4px;
bottom: -4px;
width: 6px;
left: 50%;
cursor: pointer;
border: 1px solid black;
background: white;
opacity: .8;
}
.sp-alpha {
display: none;
position: absolute;
bottom: -14px;
right: 0;
left: 0;
height: 8px;
}
.sp-alpha-inner {
border: solid 1px #333;
}
.sp-clear {
display: none;
}
.sp-clear.sp-clear-display {
background-position: center;
}
.sp-clear-enabled .sp-clear {
display: block;
position:absolute;
top:0px;
right:0;
bottom:0;
left:84%;
height: 28px;
}
/* Don't allow text selection */
.sp-container, .sp-replacer, .sp-preview, .sp-dragger, .sp-slider, .sp-alpha, .sp-clear, .sp-alpha-handle, .sp-container.sp-dragging .sp-input, .sp-container button {
-webkit-user-select:none;
-moz-user-select: -moz-none;
-o-user-select:none;
user-select: none;
}
.sp-container.sp-input-disabled .sp-input-container {
display: none;
}
.sp-container.sp-buttons-disabled .sp-button-container {
display: none;
}
.sp-container.sp-palette-buttons-disabled .sp-palette-button-container {
display: none;
}
.sp-palette-only .sp-picker-container {
display: none;
}
.sp-palette-disabled .sp-palette-container {
display: none;
}
.sp-initial-disabled .sp-initial {
display: none;
}
/* Gradients for hue, saturation and value instead of images. Not pretty... but it works */
.sp-sat {
background-image: -webkit-gradient(linear, 0 0, 100% 0, from(#FFF), to(rgba(204, 154, 129, 0)));
background-image: -webkit-linear-gradient(left, #FFF, rgba(204, 154, 129, 0));
background-image: -moz-linear-gradient(left, #fff, rgba(204, 154, 129, 0));
background-image: -o-linear-gradient(left, #fff, rgba(204, 154, 129, 0));
background-image: -ms-linear-gradient(left, #fff, rgba(204, 154, 129, 0));
background-image: linear-gradient(to right, #fff, rgba(204, 154, 129, 0));
-ms-filter: "progid:DXImageTransform.Microsoft.gradient(GradientType = 1, startColorstr=#FFFFFFFF, endColorstr=#00CC9A81)";
filter : progid:DXImageTransform.Microsoft.gradient(GradientType = 1, startColorstr='#FFFFFFFF', endColorstr='#00CC9A81');
}
.sp-val {
background-image: -webkit-gradient(linear, 0 100%, 0 0, from(#000000), to(rgba(204, 154, 129, 0)));
background-image: -webkit-linear-gradient(bottom, #000000, rgba(204, 154, 129, 0));
background-image: -moz-linear-gradient(bottom, #000, rgba(204, 154, 129, 0));
background-image: -o-linear-gradient(bottom, #000, rgba(204, 154, 129, 0));
background-image: -ms-linear-gradient(bottom, #000, rgba(204, 154, 129, 0));
background-image: linear-gradient(to top, #000, rgba(204, 154, 129, 0));
-ms-filter: "progid:DXImageTransform.Microsoft.gradient(startColorstr=#00CC9A81, endColorstr=#FF000000)";
filter : progid:DXImageTransform.Microsoft.gradient(startColorstr='#00CC9A81', endColorstr='#FF000000');
}
.sp-hue {
background: -moz-linear-gradient(top, #ff0000 0%, #ffff00 17%, #00ff00 33%, #00ffff 50%, #0000ff 67%, #ff00ff 83%, #ff0000 100%);
background: -ms-linear-gradient(top, #ff0000 0%, #ffff00 17%, #00ff00 33%, #00ffff 50%, #0000ff 67%, #ff00ff 83%, #ff0000 100%);
background: -o-linear-gradient(top, #ff0000 0%, #ffff00 17%, #00ff00 33%, #00ffff 50%, #0000ff 67%, #ff00ff 83%, #ff0000 100%);
background: -webkit-gradient(linear, left top, left bottom, from(#ff0000), color-stop(0.17, #ffff00), color-stop(0.33, #00ff00), color-stop(0.5, #00ffff), color-stop(0.67, #0000ff), color-stop(0.83, #ff00ff), to(#ff0000));
background: -webkit-linear-gradient(top, #ff0000 0%, #ffff00 17%, #00ff00 33%, #00ffff 50%, #0000ff 67%, #ff00ff 83%, #ff0000 100%);
background: linear-gradient(to bottom, #ff0000 0%, #ffff00 17%, #00ff00 33%, #00ffff 50%, #0000ff 67%, #ff00ff 83%, #ff0000 100%);
}
/* IE filters do not support multiple color stops.
Generate 6 divs, line them up, and do two color gradients for each.
Yes, really.
*/
.sp-1 {
height:17%;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff0000', endColorstr='#ffff00');
}
.sp-2 {
height:16%;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffff00', endColorstr='#00ff00');
}
.sp-3 {
height:17%;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#00ff00', endColorstr='#00ffff');
}
.sp-4 {
height:17%;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#00ffff', endColorstr='#0000ff');
}
.sp-5 {
height:16%;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#0000ff', endColorstr='#ff00ff');
}
.sp-6 {
height:17%;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff00ff', endColorstr='#ff0000');
}
.sp-hidden {
display: none !important;
}
/* Clearfix hack */
.sp-cf:before, .sp-cf:after { content: ""; display: table; }
.sp-cf:after { clear: both; }
.sp-cf { *zoom: 1; }
/* Mobile devices, make hue slider bigger so it is easier to slide */
@media (max-device-width: 480px) {
.sp-color { right: 40%; }
.sp-hue { left: 63%; }
.sp-fill { padding-top: 60%; }
}
.sp-dragger {
border-radius: 5px;
height: 5px;
width: 5px;
border: 1px solid #fff;
background: #000;
cursor: pointer;
position:absolute;
top:0;
left: 0;
}
.sp-slider {
position: absolute;
top:0;
cursor:pointer;
height: 3px;
left: -1px;
right: -1px;
border: 1px solid #000;
background: white;
opacity: .8;
}
/*
Theme authors:
Here are the basic themeable display options (colors, fonts, global widths).
See http://bgrins.github.io/spectrum/themes/ for instructions.
*/
.sp-container {
border-radius: 0;
background-color: #ECECEC;
border: solid 1px #f0c49B;
padding: 0;
}
.sp-container, .sp-container button, .sp-container input, .sp-color, .sp-hue, .sp-clear {
font: normal 12px "Lucida Grande", "Lucida Sans Unicode", "Lucida Sans", Geneva, Verdana, sans-serif;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
-ms-box-sizing: border-box;
box-sizing: border-box;
}
.sp-top {
margin-bottom: 3px;
}
.sp-color, .sp-hue, .sp-clear {
border: solid 1px #666;
}
/* Input */
.sp-input-container {
float:right;
width: 100px;
margin-bottom: 4px;
}
.sp-initial-disabled .sp-input-container {
width: 100%;
}
.sp-input {
font-size: 12px !important;
border: 1px inset;
padding: 4px 5px;
margin: 0;
width: 100%;
background:transparent;
border-radius: 3px;
color: #222;
}
.sp-input:focus {
border: 1px solid orange;
}
.sp-input.sp-validation-error {
border: 1px solid red;
background: #fdd;
}
.sp-picker-container , .sp-palette-container {
float:left;
position: relative;
padding: 10px;
padding-bottom: 300px;
margin-bottom: -290px;
}
.sp-picker-container {
width: 172px;
border-left: solid 1px #fff;
}
/* Palettes */
.sp-palette-container {
border-right: solid 1px #ccc;
}
.sp-palette-only .sp-palette-container {
border: 0;
}
.sp-palette .sp-thumb-el {
display: block;
position:relative;
float:left;
width: 24px;
height: 15px;
margin: 3px;
cursor: pointer;
border:solid 2px transparent;
}
.sp-palette .sp-thumb-el:hover, .sp-palette .sp-thumb-el.sp-thumb-active {
border-color: orange;
}
.sp-thumb-el {
position:relative;
}
/* Initial */
.sp-initial {
float: left;
border: solid 1px #333;
}
.sp-initial span {
width: 30px;
height: 25px;
border:none;
display:block;
float:left;
margin:0;
}
.sp-initial .sp-clear-display {
background-position: center;
}
/* Buttons */
.sp-palette-button-container,
.sp-button-container {
float: right;
}
/* Replacer (the little preview div that shows up instead of the <input>) */
.sp-replacer {
margin:0;
overflow:hidden;
cursor:pointer;
padding: 4px;
display:inline-block;
*zoom: 1;
*display: inline;
border: solid 1px #91765d;
background: #eee;
color: #333;
vertical-align: middle;
}
.sp-replacer:hover, .sp-replacer.sp-active {
border-color: #F0C49B;
color: #111;
}
.sp-replacer.sp-disabled {
cursor:default;
border-color: silver;
color: silver;
}
.sp-dd {
padding: 2px 0;
height: 16px;
line-height: 16px;
float:left;
font-size:10px;
}
.sp-preview {
position:relative;
width:25px;
height: 20px;
border: solid 1px #222;
margin-right: 5px;
float:left;
z-index: 0;
}
.sp-palette {
*width: 220px;
max-width: 220px;
}
.sp-palette .sp-thumb-el {
width:16px;
height: 16px;
margin:2px 1px;
border: solid 1px #d0d0d0;
}
.sp-container {
padding-bottom:0;
}
/* Buttons: http://hellohappy.org/css3-buttons/ */
.sp-container button {
background-color: #eeeeee;
background-image: -webkit-linear-gradient(top, #eeeeee, #cccccc);
background-image: -moz-linear-gradient(top, #eeeeee, #cccccc);
background-image: -ms-linear-gradient(top, #eeeeee, #cccccc);
background-image: -o-linear-gradient(top, #eeeeee, #cccccc);
background-image: linear-gradient(to bottom, #eeeeee, #cccccc);
border: 1px solid #ccc;
border-bottom: 1px solid #bbb;
border-radius: 3px;
color: #333;
font-size: 14px;
line-height: 1;
padding: 5px 4px;
text-align: center;
text-shadow: 0 1px 0 #eee;
vertical-align: middle;
}
.sp-container button:hover {
background-color: #dddddd;
background-image: -webkit-linear-gradient(top, #dddddd, #bbbbbb);
background-image: -moz-linear-gradient(top, #dddddd, #bbbbbb);
background-image: -ms-linear-gradient(top, #dddddd, #bbbbbb);
background-image: -o-linear-gradient(top, #dddddd, #bbbbbb);
background-image: linear-gradient(to bottom, #dddddd, #bbbbbb);
border: 1px solid #bbb;
border-bottom: 1px solid #999;
cursor: pointer;
text-shadow: 0 1px 0 #ddd;
}
.sp-container button:active {
border: 1px solid #aaa;
border-bottom: 1px solid #888;
-webkit-box-shadow: inset 0 0 5px 2px #aaaaaa, 0 1px 0 0 #eeeeee;
-moz-box-shadow: inset 0 0 5px 2px #aaaaaa, 0 1px 0 0 #eeeeee;
-ms-box-shadow: inset 0 0 5px 2px #aaaaaa, 0 1px 0 0 #eeeeee;
-o-box-shadow: inset 0 0 5px 2px #aaaaaa, 0 1px 0 0 #eeeeee;
box-shadow: inset 0 0 5px 2px #aaaaaa, 0 1px 0 0 #eeeeee;
}
.sp-cancel {
font-size: 11px;
color: #d93f3f !important;
margin:0;
padding:2px;
margin-right: 5px;
vertical-align: middle;
text-decoration:none;
}
.sp-cancel:hover {
color: #d93f3f !important;
text-decoration: underline;
}
.sp-palette span:hover, .sp-palette span.sp-thumb-active {
border-color: #000;
}
.sp-preview, .sp-alpha, .sp-thumb-el {
position:relative;
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAIAAADZF8uwAAAAGUlEQVQYV2M4gwH+YwCGIasIUwhT25BVBADtzYNYrHvv4gAAAABJRU5ErkJggg==);
}
.sp-preview-inner, .sp-alpha-inner, .sp-thumb-inner {
display:block;
position:absolute;
top:0;left:0;bottom:0;right:0;
}
.sp-palette .sp-thumb-inner {
background-position: 50% 50%;
background-repeat: no-repeat;
}
.sp-palette .sp-thumb-light.sp-thumb-active .sp-thumb-inner {
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABIAAAASCAYAAABWzo5XAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAIVJREFUeNpiYBhsgJFMffxAXABlN5JruT4Q3wfi/0DsT64h8UD8HmpIPCWG/KemIfOJCUB+Aoacx6EGBZyHBqI+WsDCwuQ9mhxeg2A210Ntfo8klk9sOMijaURm7yc1UP2RNCMbKE9ODK1HM6iegYLkfx8pligC9lCD7KmRof0ZhjQACDAAceovrtpVBRkAAAAASUVORK5CYII=);
}
.sp-palette .sp-thumb-dark.sp-thumb-active .sp-thumb-inner {
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABIAAAASCAYAAABWzo5XAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAadEVYdFNvZnR3YXJlAFBhaW50Lk5FVCB2My41LjEwMPRyoQAAAMdJREFUOE+tkgsNwzAMRMugEAahEAahEAZhEAqlEAZhEAohEAYh81X2dIm8fKpEspLGvudPOsUYpxE2BIJCroJmEW9qJ+MKaBFhEMNabSy9oIcIPwrB+afvAUFoK4H0tMaQ3XtlrggDhOVVMuT4E5MMG0FBbCEYzjYT7OxLEvIHQLY2zWwQ3D+9luyOQTfKDiFD3iUIfPk8VqrKjgAiSfGFPecrg6HN6m/iBcwiDAo7WiBeawa+Kwh7tZoSCGLMqwlSAzVDhoK+6vH4G0P5wdkAAAAASUVORK5CYII=);
}
.sp-clear-display {
background-repeat:no-repeat;
background-position: center;
background-image: url(data:image/gif;base64,R0lGODlhFAAUAPcAAAAAAJmZmZ2dnZ6enqKioqOjo6SkpKWlpaampqenp6ioqKmpqaqqqqurq/Hx8fLy8vT09PX19ff39/j4+Pn5+fr6+vv7+wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAAP8ALAAAAAAUABQAAAihAP9FoPCvoMGDBy08+EdhQAIJCCMybCDAAYUEARBAlFiQQoMABQhKUJBxY0SPICEYHBnggEmDKAuoPMjS5cGYMxHW3IiT478JJA8M/CjTZ0GgLRekNGpwAsYABHIypcAgQMsITDtWJYBR6NSqMico9cqR6tKfY7GeBCuVwlipDNmefAtTrkSzB1RaIAoXodsABiZAEFB06gIBWC1mLVgBa0AAOw==);
}

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

@ -4,6 +4,7 @@ SCENARIO_IMAGES_SOURCE_INTERNET = 2 ;
gUserSettings = Cookies.getJSON( "user-settings" ) || { "scenario-images-source": SCENARIO_IMAGES_SOURCE_INTERNET } ;
USER_SETTINGS = {
"vasl-username": "text",
"snippet-font-family": "text",
"snippet-font-size": "text",
"date-format": "droplist",
@ -77,7 +78,7 @@ function user_settings()
dialogClass: "user-settings",
modal: true,
width: 460,
height: 350,
height: 375,
resizable: false,
create: function() {
init_dialog( $(this), "OK", true ) ;
@ -114,12 +115,9 @@ function user_settings()
buttons: {
OK: function() {
// unload and install the new user settings
var settings = unload_settings() ;
gUserSettings = settings ;
Cookies.set( "user-settings", settings, { expires: 999 } ) ;
gUserSettings = unload_settings() ;
save_user_settings() ;
apply_user_settings() ;
if ( gWebChannelHandler )
gWebChannelHandler.on_user_settings_change( JSON.stringify( settings ) ) ;
$(this).dialog( "close" ) ;
},
Cancel: function() { $(this).dialog( "close" ) ; },
@ -129,6 +127,16 @@ function user_settings()
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
function save_user_settings()
{
// save the user settings
Cookies.set( "user-settings", gUserSettings, { expires: 999 } ) ;
if ( gWebChannelHandler )
gWebChannelHandler.on_user_settings_change( JSON.stringify( gUserSettings ) ) ;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
function apply_user_settings()
{
// set the date format

@ -378,6 +378,15 @@ function add_flag_to_dialog_titlebar( $dlg, player_no )
// --------------------------------------------------------------------
function getElemSizeAndPosition( $elem )
{
// return the element's size and position
return {
width: $elem.width(), height: $elem.height(),
left: $elem.offset().left, top: $elem.offset().top,
} ;
}
function fixup_external_links( $root )
{
// NOTE: We want to open externals links in a new browser window, but simply adding target="_blank"
@ -485,6 +494,13 @@ function getFilenameExtn( fname )
return null ;
}
function stopEvent( evt )
{
// stop further processing for the event
evt.preventDefault() ;
evt.stopPropagation() ;
}
function isIE()
{
// check if we're running in IE :-/

@ -22,9 +22,9 @@ function _do_update_vsav( vsav_data, fname )
type: "POST",
data: JSON.stringify( data ),
contentType: "application/json",
} ).done( function( data ) {
} ).done( function( resp ) {
$dlg.dialog( "close" ) ;
data = _check_vassal_shim_response( data, "Can't update the VASL scenario." ) ;
data = _check_vassal_shim_response( resp, "Can't update the VASL scenario." ) ;
if ( ! data )
return ;
// check if anything was changed
@ -322,9 +322,9 @@ function _do_analyze_vsav( vsav_data, fname )
type: "POST",
data: JSON.stringify( data ),
contentType: "application/json",
} ).done( function( data ) {
} ).done( function( resp ) {
$dlg.dialog( "close" ) ;
data = _check_vassal_shim_response( data, "Can't analyze the VASL scenario." ) ;
data = _check_vassal_shim_response( resp, "Can't analyze the VASL scenario." ) ;
if ( ! data )
return ;
_create_vo_entries_from_analysis( data ) ;
@ -479,33 +479,43 @@ function on_load_vsav_file_selected()
// --------------------------------------------------------------------
function _check_vassal_shim_response( data, caption )
function _check_vassal_shim_response( resp, caption )
{
// check if there was an error
if ( ! data.error )
return data ;
if ( ! resp.error )
return resp ;
// yup - report the error
if ( getUrlParam( "vsav_persistence" ) ) {
$( "#_vsav-persistence_" ).val(
"ERROR: " + data.error + "\n\n=== STDOUT ===\n" + data.stdout + "\n=== STDERR ===\n" + data.stderr
"ERROR: " + resp.error + "\n\n=== STDOUT ===\n" + resp.stdout + "\n=== STDERR ===\n" + resp.stderr
) ;
return null ;
}
show_vassal_shim_error_dlg( resp, caption ) ;
return null ;
}
function show_vassal_shim_error_dlg( resp, caption )
{
// show the VASSAL shim error dialog
if ( caption[ caption.length-1 ] == "." )
caption = caption.substring( 0, caption.length-1 ) ;
$( "#vassal-shim-error" ).dialog( {
dialogClass: "vassal-shim-error",
title: caption,
modal: true,
width: 600, height: "auto",
open: function() {
$( "#vassal-shim-error .message" ).html( data.error ) ;
$( "#vassal-shim-error .message" ).html( resp.error ) ;
var log = "" ;
if ( data.stdout && data.stderr )
log = "=== STDOUT ===\n" + data.stdout + "\n=== STDERR ===\n" + data.stderr ;
else if ( data.stdout )
log = data.stdout ;
else if ( data.stderr )
log = data.stderr ;
if ( resp.stdout && resp.stderr )
log = "=== STDOUT ===\n" + resp.stdout + "\n=== STDERR ===\n" + resp.stderr ;
else if ( resp.stdout )
log = resp.stdout ;
else if ( resp.stderr )
log = resp.stderr ;
if ( log )
$( "#vassal-shim-error .log" ).val( log ).show() ;
else
@ -515,8 +525,6 @@ function _check_vassal_shim_response( data, caption )
Close: function() { $(this).dialog( "close" ) ; },
},
} ) ;
return null ;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

@ -10,7 +10,9 @@
<link rel="stylesheet" type="text/css" href="{{url_for('static',filename='jquery-ui/jquery-ui.min.css')}}" />
<link rel="stylesheet" type="text/css" href="{{url_for('static',filename='growl/jquery.growl.css')}}" />
<link rel="stylesheet" type="text/css" href="{{url_for('static',filename='popmenu/jquery.popmenu.css')}}" />
<link rel="stylesheet" type="text/css" href="{{url_for('static',filename='spectrum/spectrum.css')}}" />
<link rel="stylesheet" type="text/css" href="{{url_for('static',filename='select2/select2.min.css')}}" />
<link rel="stylesheet" type="text/css" href="{{url_for('static',filename='chartjs/Chart.min.css')}}" />
<link rel="stylesheet" type="text/css" href="{{url_for('static',filename='css/main.css')}}" />
<link rel="stylesheet" type="text/css" href="{{url_for('static',filename='css/tabs.css')}}" />
<link rel="stylesheet" type="text/css" href="{{url_for('static',filename='css/tabs-scenario.css')}}" />
@ -24,6 +26,8 @@
<link rel="stylesheet" type="text/css" href="{{url_for('static',filename='css/edit-vo-dialog.css')}}" />
<link rel="stylesheet" type="text/css" href="{{url_for('static',filename='css/select-roar-scenario-dialog.css')}}" />
<link rel="stylesheet" type="text/css" href="{{url_for('static',filename='css/vassal-shim.css')}}" />
<link rel="stylesheet" type="text/css" href="{{url_for('static',filename='css/lfa.css')}}" />
<link rel="stylesheet" type="text/css" href="{{url_for('static',filename='css/lfa-upload.css')}}" />
<link rel="stylesheet" type="text/css" href="{{url_for('static',filename='css/snippets.css')}}" />
<link rel="stylesheet" type="text/css" href="{{url_for('static',filename='css/user-settings-dialog.css')}}" />
@ -43,13 +47,15 @@
<div id="menu" style="display:none;">
<input type="image" src="{{url_for('static',filename='images/menu.png')}}" value="actions">
<input id="load-scenario" type="file" accept=".json" style="display:none;">
<input id="load-template-pack" type="file" accept=".zip,.j2" style="display:none;">
<input id="load-vsav" type="file" accept=".vsav" style="display:none;">
</div>
{%include "tabs.html"%}
<input id="load-scenario" type="file" accept=".json" style="display:none;">
<input id="load-template-pack" type="file" accept=".zip,.j2" style="display:none;">
<input id="load-vsav" type="file" accept=".vsav" style="display:none;">
<input id="load-vlog" type="file" multiple accept=".vlog" style="display:none;">
<div id="alt-webapp-base-url" style="display:none;"> Webapp base URL: </div>
<!-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -->
@ -69,6 +75,8 @@
{%include "select-roar-scenario-dialog.html"%}
{%include "vassal.html"%}
{%include "lfa.html"%}
{%include "lfa-upload.html"%}
{%include "snippets.html"%}
{%include "user-settings-dialog.html"%}
@ -85,10 +93,14 @@
<script src="{{url_for('static',filename='growl/jquery.growl.js')}}"></script>
<script src="{{url_for('static',filename='popmenu/jquery.popmenu-1.0.0.min.js')}}"></script>
<script src="{{url_for('static',filename='hotkey/jquery.hotkey.js')}}"></script>
<script src="{{url_for('static',filename='spectrum/spectrum.js')}}"></script>
<script src="{{url_for('static',filename='download/download.min.js')}}"></script>
<script src="{{url_for('static',filename='jszip/jszip.min.js')}}"></script>
<script src="{{url_for('static',filename='select2/select2.min.js')}}"></script>
<script src="{{url_for('static',filename='split/split.min.js')}}"></script>
<script src="{{url_for('static',filename='js-cookie/js.cookie.js')}}"></script>
<script src="{{url_for('static',filename='chartjs/Chart.min.js')}}"></script>
<script src="{{url_for('static',filename='chartjs/chartjs-plugin-labels.min.js')}}"></script>
<script>
gAppName = "{{APP_NAME}}" ;
@ -105,6 +117,7 @@ gOrdnanceNotesUrl = "{{url_for('get_ordnance_notes')}}" ;
gGetVaslPieceInfoUrl = "{{url_for('get_vasl_piece_info')}}" ;
gGetRoarScenarioIndexUrl = "{{url_for('get_roar_scenario_index')}}" ;
gAnalyzeVsavUrl = "{{url_for('analyze_vsav')}}" ;
gAnalyzeVlogsUrl = "{{url_for('analyze_vlogs')}}" ;
gUpdateVsavUrl = "{{url_for('update_vsav')}}" ;
gMakeSnippetImageUrl = "{{url_for('make_snippet_image')}}" ;
gHelpUrl = "{{url_for('show_help')}}" ;
@ -118,9 +131,13 @@ gHelpUrl = "{{url_for('show_help')}}" ;
<script src="{{url_for('static',filename='vo.js')}}"></script>
<script src="{{url_for('static',filename='vo2.js')}}"></script>
<script src="{{url_for('static',filename='vassal.js')}}"></script>
<script src="{{url_for('static',filename='lfa.js')}}"></script>
<script src="{{url_for('static',filename='lfa-upload.js')}}"></script>
<script src="{{url_for('static',filename='LogFileAnalysis.js')}}"></script>
<script src="{{url_for('static',filename='roar.js')}}"></script>
<script src="{{url_for('static',filename='sortable.js')}}"></script>
<script src="{{url_for('static',filename='user_settings.js')}}"></script>
<script src="{{url_for('static',filename='jQueryHandlers.js')}}"></script>
<script src="{{url_for('static',filename='utils.js')}}"></script>
{%include "testing.html"%}

@ -0,0 +1,13 @@
<div id="lfa-upload" style="display:none;">
<ul class="files"> </ul>
<div class="hint">
<div class="content">
<img src="{{url_for('static',filename='images/lfa/file.png')}}">
Click here to add your VASL log files, <br>
or just drag them in.
</div>
</div>
</div>

@ -0,0 +1,76 @@
<div id="lfa" style="display:none;">
<div class="split top-pane"> <!-- top pane -->
<div class="left"> <!-- left column -->
<div class="banner">
<div> <span class="title"></span> <span class="title2"></span> </div>
<div class="roll-type"> </div>
<div class="select-file"></div>
</div>
<div class="distrib d6x1"> <canvas></canvas> </div>
<div class="pie d6x1"> <canvas></canvas> </div>
</div> <!-- end left column -->
<div class="right"> <!-- right column -->
<div class="distrib d6x2"> <canvas></canvas> </div>
<div class="pie d6x2"> <canvas></canvas> </div>
<div class="hotness" title="How hot were the dice...?" style="display:none;">
<img src="{{url_for('static',filename='images/lfa/hotness.png')}}" class="dice">
<canvas></canvas>
</div>
</div> <!-- end right column -->
</div> <!-- end top pane -->
<div class="split bottom-pane"> <!-- bottom pane -->
<div class="time-plot-options" style="display:none;text-align:right;">
<label for="moving-average"> Moving avera<u>g</u>e:</label> <select name="moving-average"></select>
&nbsp;
<button class="zoom-in"> <img src="{{url_for('static',filename='images/lfa/plus.png')}}" style="height:1em;" title="Zoom in"> </button>
<button class="zoom-out"> <img src="{{url_for('static',filename='images/lfa/minus.png')}}" style="height:1em;" title="Zoom out"> </button>
</div>
<div class="time-plot">
<div class="wrapper"> <canvas></canvas> </div>
</div>
</div> <!-- end bottom pane -->
<div class="options">
<div>
<label name="roll-type"> <u>R</u>oll type: </label>
<select name="roll-type"> </select>
</div>
<input type="checkbox" name="stack-bar-graphs"> Stack bar graphs <br>
<input type="checkbox" name="disable-animations"> No animations <br>
<button class="player-colors" style="display:flex;">
<img src="{{url_for('static',filename='images/lfa/player-colors.png')}}" style="height:1em;"> <span style="display:inline-block;margin-left:0.25em;">Colors</span>
</button>
<button class="download" style="display:flex;" title="Download the data">
<img src="{{url_for('static',filename='images/lfa/download.png')}}" style="height:1.5em;">
</button>
</div>
<div class="player-colors-popup" style="display:none;"> </div>
<div class="select-file-popup" style="display:none;"> </div>
<div class="timePlot-tooltip" style="border-radius:5px;z-index:99;">
<table style="font-size:100%;"> </table>
</div>
<button type="button" class="ui-button ui-corner-all ui-widget ui-button-icon-only ui-dialog-titlebar-close" title="Close" style="z-index:99;">
<span class="ui-button-icon ui-icon ui-icon-closethick"></span>
</button>
</div>

@ -6,5 +6,7 @@
<textarea id="_template-pack-persistence_" style="display:none;"></textarea>
<textarea id="_scenario-persistence_" style="display:none;"></textarea>
<textarea id="_vsav-persistence_" style="display:none;"></textarea>
<textarea id="_vlog-persistence_" style="display:none;"></textarea>
<textarea id="_lfa-download_" style="display:none;"></textarea>
<textarea id="_snippet-image-persistence_" style="display:none;"></textarea>
<button id="popmenu-hack" style="display:none;position:absolute;top:0;left:0;">

@ -1,7 +1,12 @@
<div id="user-settings" style="display:none;">
<div class="row" style="display:flex;align-items:center;">
<label for="vasl-username" style="display:inline-block;width:8.75em;"> VASL username: </label>
<input type="text" name="vasl-username" style="flex:1;">
</div>
<div class="row" style="display:flex;align-items:center;margin-bottom:0.25em;">
HTML snippet font:&nbsp;
<label for="snippet-font-family" style="width:8.75em;"> HTML snippet font: </label>
<input type="text" name="snippet-font-family" title='CSS font family (e.g. "Verdana" or "sans-serif")' style="flex-grow:1;">&nbsp;
<input type="text" name="snippet-font-size" size="3" title='CSS font size (e.g. "12px")'>
</div>

@ -0,0 +1,589 @@
""" Test log file analysis. """
import os
import base64
import csv
import pytest
from selenium.webdriver.support.ui import Select
from vasl_templates.webapp.tests.utils import init_webapp, select_menu_option, \
wait_for, wait_for_elem, find_child, find_children, set_stored_msg, set_stored_msg_marker, get_stored_msg, \
get_droplist_vals, select_droplist_val, unload_table
from vasl_templates.webapp.tests.test_vassal import _run_tests
# ---------------------------------------------------------------------
@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
def test_full( webapp, webdriver ):
"""Test a full log file analysis."""
# initialize
control_tests = init_webapp( webapp, webdriver, vlog_persistence=1, lfa_tables=1 )
def do_test(): #pylint: disable=missing-docstring
# analyze the log file
# === RPh === === PFPh === === MPh === === DFPh ===
# A1: Other 5 4 A6: IFT 3 1 B5: IFT 4 2
# A2: Rally 4 1 B3: MC 5 2 b5: sa 2
# A3: Rally 3 1 A7: IFT 5 3 A12: MC 1 2
# B1: Rally 6 2 b3: sa 4
# A4: Rally 6 4 b4: rs 6
# a1: dr 6 B4: MC 1 6
# A5: Rally 3 4 A8: TH 5 5
# B2: Rally 5 3 A9: TK 2 3
# b1: dr 2 A10: IFT 3 3
# b2: dr 2 A11: IFT 4 4
_analyze_vlogs( "full.vlog" )
# check the results
lfa = _get_chart_data( 1 )
assert lfa["distrib"]["dr"] == [
[ "Alice (6.0)", "Bob (3.2)" ],
["",""], ["","60"], ["",""], ["","20"], ["",""], ["100","20"]
]
assert lfa["distrib"]["DR"] == [
[ "Alice (6.6)", "Bob (7.2)" ],
["",""], ["8.3",""], ["16.7",""], ["16.7",""], ["8.3","20"],
["8.3","40"], ["16.7","40"], ["8.3",""], ["16.7",""], ["",""], ["",""]
]
# check the results
assert lfa["pie"]["dr"] == [ ["Bob","5"], ["Alice","1"] ]
assert lfa["pie"]["DR"] == [ ["Bob","5"], ["Alice","12"] ]
# check the results
_check_time_plot_window_sizes( [ 1, 5 ] )
assert lfa["timePlot"] == [
[ "", "Alice (12)", "Bob (5)" ],
["","9",""], ["","5",""], ["","4",""], ["","","8"], ["","10",""],
["","7",""], ["","","8"],
[ "Axis 1 PFPh", "4", "" ],
["","","7"], ["","8",""], ["","","7"], ["","10",""], ["","5",""],
["","6",""], ["","8",""],
[ "Axis 1 DFPh", "","6" ],
["","3",""]
]
_check_time_plot_values( [1,5], "5", [
[ "", "Alice (12)", "Bob (5)" ],
["","7",""], ["Axis 1 PFPh","6",""], ["","6.6",""], ["","7.8",""], ["","6.8",""],
["","6.6",""], ["","7.4",""],
[ "Axis 1 DFPh", "", "7.2" ],
["","6.4",""]
] )
# check the results
assert lfa["hotness"] == [ ["Alice","1.367"], ["Bob","-0.927"] ]
# switch to showing the Morale Check DR's and check the results
_select_roll_type( "MC" )
lfa = _get_chart_data()
assert lfa["distrib"]["dr"] == []
assert lfa["distrib"]["DR"] == [
[ "Alice (3.0)", "Bob (7.0)" ],
["",""], ["100",""], ["",""], ["",""], ["",""],
["","100"], ["",""], ["",""], ["",""], ["",""], ["",""]
]
assert lfa["pie"]["dr"] == []
assert lfa["pie"]["DR"] == [ ["Bob","2"], ["Alice","1"] ]
_check_time_plot_values( [1], "1", [
[ "", "Alice (1)", "Bob (2)" ],
[ "Axis 1 PFPh", "", "7" ],
["","","7"],
[ "Axis 1 DFPh", "3", "" ],
] )
assert lfa["hotness"] == [ ["Alice","287.445"], ["Bob","0.000"] ]
# switch to showing the Sniper Activation DR's and check the results
_select_roll_type( "SA" )
lfa = _get_chart_data()
assert lfa["distrib"]["dr"] == [
[ "Bob (3.0)" ],
[""], ["50"], [""], ["50"], [""], [""]
]
assert lfa["distrib"]["DR"] == []
assert lfa["pie"]["dr"] == [ ["Bob","2"] ]
assert lfa["pie"]["DR"] == []
_check_time_plot_values( [1], 1, [
[ "", "Bob (2)" ],
[ "Axis 1 PFPh", "4" ],
[ "Axis 1 DFPh", "2" ],
] )
assert lfa["hotness"] == [ ["Alice",""], ["Bob",""] ]
# switch to showing the Close Combat DR's and check the results
_select_roll_type( "CC" )
lfa = _get_chart_data()
assert lfa["distrib"]["dr"] == []
assert lfa["distrib"]["DR"] == []
assert lfa["pie"]["dr"] == []
assert lfa["pie"]["DR"] == []
_check_time_plot_values( [1], 1, [] )
assert lfa["hotness"] == [ ["Alice",""], ["Bob",""] ]
# close the analysis window
find_child( "#lfa button.ui-dialog-titlebar-close" ).click()
# run the tests
_run_tests( control_tests, do_test, not pytest.config.option.short_tests ) #pylint: disable=no-member
# ---------------------------------------------------------------------
@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
def test_4players( webapp, webdriver ):
"""Test a file log file analysis with 4 players."""
# initialize
control_tests = init_webapp( webapp, webdriver, vlog_persistence=1, lfa_tables=1 )
def do_test(): #pylint: disable=missing-docstring
# analyze the log file
# RPh PFPh MPh DFPh
# --- ----- ------ ------
# A1: 3 C1: 4 D3: 1 C6: 2
# B1: 3 C2: 4 D4: 3 D7: 3
# A2: 6 C3: 2 D5: 1 D8: 6
# A3: 1 B5: 5 A6: 4 A11: 2
# B2: 6 B6: 5 A7: 1 D9: 1
# A4: 5 D2: 1 A8: 4 D10: 2
# B3: 2 C4: 3 A9: 1
# B4: 4 C5: 4 A10: 1
# A5: 2 B7: 6
# D1: 4 B8: 5
# D6: 6
_analyze_vlogs( "4players.vlog" )
# check the results
lfa = _get_chart_data( 1 )
assert lfa["distrib"]["dr"] == [
[ "Alice (2.7)", "Bob (4.5)", "Dave (2.8)", "Chuck (3.2)" ],
[ "36.4", "", "40", "" ],
[ "18.2", "12.5", "10", "33.3" ],
[ "9.1", "12.5", "20", "16.7" ],
[ "18.2", "12.5", "10", "50" ],
[ "9.1", "37.5", "", "" ],
[ "9.1", "25", "20", "" ]
]
assert lfa["distrib"]["DR"] == []
# check the results
assert lfa["pie"]["dr"] == [ ["Chuck","6"], ["Dave","10"], ["Bob","8"], ["Alice","11"] ]
assert lfa["pie"]["DR"] == []
# check the results
assert lfa["timePlot"] == []
# switch to showing the Random Selection dr's and check the results
_select_roll_type( "RS" )
lfa = _get_chart_data( 1 )
_check_time_plot_window_sizes( [ 1, 5 ] )
assert lfa["timePlot"] == [
[ "", "Alice (11)", "Bob (8)", "Dave (10)", "Chuck (6)" ],
["","3","","",""], ["","","3","",""], ["","6","","",""], ["","1","","",""], ["","","6","",""],
["","5","","",""], ["","","2","",""], ["","","4","",""], ["","2","","",""], ["","","","4",""],
[ "Allied 1 PFPh", "", "", "", "4" ],
["","","","","4"], ["","","","","2"], ["","","5","",""], ["","","5","",""], ["","","","1",""],
["","","","","3"], ["","","","","4"],
[ "Allied 1 MPh", "", "", "1", "" ],
["","","","3",""], ["","","","1",""], ["","4","","",""], ["","1","","",""], ["","4","","",""],
["","1","","",""], ["","1","","",""], ["","","6","",""], ["","","5","",""], ["","","","6",""],
[ "Allied 1 DFPh", "", "", "", "2" ],
["","","","3",""], ["","","","6",""], ["","2","","",""], ["","","","1",""], ["","","","2",""]
]
lfa = _get_chart_data( 5 )
assert lfa["timePlot"] == [
[ "", "Alice (11)", "Bob (8)", "Dave (10)", "Chuck (6)" ],
["","3.4","","",""],
["Allied 1 PFPh","","4","",""],
["","","4.4","",""], ["","","","","3.4"],
["Allied 1 MPh","","","2",""],
["","3.6","","",""], ["","2.6","","",""], ["","3.2","","",""], ["","2.4","","",""], ["","2.2","","",""],
["","","4.4","",""], ["","","5","",""], ["","","","2.4",""],
["Allied 1 DFPh","","","","3"],
["","","","2.8",""], ["","","","3.8",""], ["","1.8","","",""], ["","","","3.4",""], ["","","","3.6",""]
]
# close the analysis window
find_child( "#lfa button.ui-dialog-titlebar-close" ).click()
# run the tests
_run_tests( control_tests, do_test, not pytest.config.option.short_tests ) #pylint: disable=no-member
# ---------------------------------------------------------------------
@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
def test_multiple_files( webapp, webdriver ):
"""Test analyzing multiple log files."""
# initialize
control_tests = init_webapp( webapp, webdriver, vlog_persistence=1, lfa_tables=1 )
def check_color_pickers( expected ):
"""Check which color pickers are being presented to the user."""
find_child( "#lfa .options button.player-colors" ).click()
popup = wait_for_elem( 2, "#lfa .player-colors-popup" )
player_names = [ e.text for e in find_children( ".row .caption", popup ) if e.text ]
assert player_names.pop() == "expected results"
assert player_names == expected
def select_file( fname ):
"""Select one of the files being analyzed."""
find_child( "#lfa .banner .select-file" ).click()
popup = wait_for_elem( 2, "#lfa .select-file-popup" )
for row in find_children( ".row", popup ):
if find_child( "label", row ).text == fname:
find_child( "input[type='radio']", row ).click()
return
assert False, "Couldn't find file: "+fname
def do_test(): #pylint: disable=missing-docstring
# NOTE: The "1a" and "1b" log files have the same players (Alice and Bob), but the "2" log file
# has Bob and Chuck.
# multiple-1a multiple-1b multiple-2
# ----------- ----------- ----------
# A: IFT 5 2 A: IFT 3 6 B: IFT 5 5
# B: IFT 2 6 B: IFT 4 2 C: IFT 6 5
# A: rs 6 A: rs 2 B: sa 4
# A: IFT 4 1 Turn Track Turn Track
# B: IFT 4 4 A: IFT 4 3 B: IFT 2 2
# B: rs 1 B: IFT 6 4 C: IFT 5 4
# Turn Track C: rs 5
# A: IFT 2 1
# B: IFT 4 4
# A: rs 2
# B: sa 5
# load 2 log files that have the same players
_analyze_vlogs( [ "multiple-1a.vlog", "multiple-1b.vlog" ] )
# check the results
lfa = _get_chart_data( 1 )
assert lfa["timePlot"] == [
[ "", "Alice (5)", "Bob (5)" ],
["","7",""], ["","","8"], ["","5",""], ["","","8"],
[ "Allied 1 PFPh", "3", "" ],
["","","8"], ["","9",""], ["","","6"],
[ "Allied 1 MPh", "7", "" ],
["","","10"],
]
assert lfa["hotness"] == [ ["Alice","7.673"], ["Bob","-5.484"] ]
_select_roll_type( "RS" )
lfa = _get_chart_data( 1 )
assert lfa["timePlot"] == [
[ "", "Alice (3)", "Bob (1)" ],
["","6",""], ["","","1"],
[ "Allied 1 PFPh", "2", "" ],
["","2",""],
]
_select_roll_type( "SA" )
lfa = _get_chart_data( 1 )
assert lfa["timePlot"] == [
[ "", "Bob (1)" ],
[ "Allied 1 PFPh", "5" ],
]
# close the analysis window
find_child( "#lfa button.ui-dialog-titlebar-close" ).click()
# load 2 log files that have different players
_analyze_vlogs( [ "multiple-1a.vlog", "multiple-2.vlog" ] )
def check_all_files():
"""Check the results for all files."""
lfa = _get_chart_data( 1 )
assert lfa["timePlot"] == [
[ "", "Alice (3)", "Bob (5)", "Chuck (2)" ],
["","7","",""], ["","","8",""], ["","5","",""], ["","","8",""],
[ "Allied 1 PFPh", "3", "", "" ],
["","","8",""], ["","","10",""], ["","","","11"],
[ "UN 1 PFPh", "", "4", "" ],
["","","","9"],
]
assert lfa["hotness"] == [ ["Alice","28.512"], ["Bob","-3.336"], ["Chuck","-71.744"] ]
_select_roll_type( "RS" )
lfa = _get_chart_data( 1 )
assert lfa["timePlot"] == [
[ "", "Alice (2)", "Bob (1)", "Chuck (1)" ],
["","6","",""], ["","","1",""],
[ "Allied 1 PFPh", "2", "", "" ],
["UN 1 PFPh","","","5"],
]
_select_roll_type( "SA" )
lfa = _get_chart_data( 1 )
assert lfa["timePlot"] == [
[ "", "Bob (2)" ],
[ "Allied 1 PFPh", "5" ],
["","4"],
]
check_all_files()
check_color_pickers( [ "Alice", "Bob", "Chuck" ] )
# select a file and check the results
select_file( "multiple-1a.vlog" )
_select_roll_type( "" )
lfa = _get_chart_data( 1 )
assert lfa["timePlot"] == [
[ "", "Alice (3)", "Bob (3)" ],
["","7",""], ["","","8"], ["","5",""], ["","","8"],
[ "Allied 1 PFPh", "3", "" ],
["","","8"],
]
assert lfa["hotness"] == [ ["Alice","28.512"], ["Bob","-10.944"] ]
_select_roll_type( "RS" )
lfa = _get_chart_data( 1 )
assert lfa["timePlot"] == [
[ "", "Alice (2)", "Bob (1)" ],
["","6",""], ["","","1"],
[ "Allied 1 PFPh", "2", "" ],
]
_select_roll_type( "SA" )
lfa = _get_chart_data( 1 )
assert lfa["timePlot"] == [
[ "", "Bob (1)" ],
[ "Allied 1 PFPh", "5" ],
]
check_color_pickers( [ "Alice", "Bob" ] )
# select another file and check the results
select_file( "multiple-2.vlog" )
_select_roll_type( "" )
lfa = _get_chart_data( 1 )
assert lfa["timePlot"] == [
[ "", "Bob (2)", "Chuck (2)" ],
["","10",""], ["","","11"],
[ "UN 1 PFPh", "4", "" ],
["","","9"],
]
assert lfa["hotness"] == [ ["Bob","0.000"], ["Chuck","-71.744"] ]
_select_roll_type( "RS" )
lfa = _get_chart_data( 1 )
assert lfa["timePlot"] == [
[ "", "Chuck (1)" ],
[ "UN 1 PFPh", "5" ],
]
_select_roll_type( "SA" )
lfa = _get_chart_data( 1 )
assert lfa["timePlot"] == [
[ "", "Bob (1)" ],
["","4"],
]
check_color_pickers( [ "Bob", "Chuck" ] )
# select all files and check the results
select_file( "All files" )
_select_roll_type( "" )
check_all_files()
check_color_pickers( [ "Alice", "Bob", "Chuck" ] )
# close the analysis window
find_child( "#lfa button.ui-dialog-titlebar-close" ).click()
# run the tests
_run_tests( control_tests, do_test, not pytest.config.option.short_tests ) #pylint: disable=no-member
# ---------------------------------------------------------------------
@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
def test_3d6( webapp, webdriver ):
"""Test scenarios that use the 3d6 extension."""
# initialize
control_tests = init_webapp( webapp, webdriver, vlog_persistence=1, lfa_tables=1 )
def do_test(): #pylint: disable=missing-docstring
# analyze the log file
_analyze_vlogs( "3d6.vlog" )
# check the results
# IFT 6,6
# RS 2
# 3d6 3,4,1
# IFT 6,5
# TH 6,2
# 3d6 2,4,2
lfa = _get_chart_data( 1 )
assert lfa["timePlot"] == [
[ "", "test (5)" ],
["","12"], ["","7"], ["","11"], ["","8"], ["","6"]
]
_select_roll_type( "3d6 (DR)" )
lfa = _get_chart_data()
assert lfa["timePlot"] == [
[ "", "test (2)" ],
["","7"], ["","6"]
]
_select_roll_type( "3d6 (dr)" )
lfa = _get_chart_data()
assert lfa["timePlot"] == [
[ "", "test (2)" ],
["","1"], ["","2"]
]
# close the analysis window
find_child( "#lfa button.ui-dialog-titlebar-close" ).click()
# run the tests
_run_tests( control_tests, do_test, not pytest.config.option.short_tests ) #pylint: disable=no-member
# ---------------------------------------------------------------------
@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
def test_banner_updates( webapp, webdriver ):
"""Test updating the banner."""
# initialize
control_tests = init_webapp( webapp, webdriver, vlog_persistence=1 )
def check_banner( roll_type ):
"""Check the banner."""
assert find_child( "#lfa .banner .title" ).text == "Log File Analysis test"
assert find_child( "#lfa .banner .title2" ).text == "(LFA-1)"
assert find_child( "#lfa .banner .roll-type" ).text == roll_type
def do_test(): #pylint: disable=missing-docstring
# analyze the log file
_analyze_vlogs( "banner-updates.vlog" )
# check the banner as the roll type is changed
check_banner( "Showing all rolls." )
_select_roll_type( "MC" )
check_banner( "Showing Morale Check rolls." )
_select_roll_type( "RS" )
check_banner( "Showing Random Selection rolls." )
# close the analysis window
find_child( "#lfa button.ui-dialog-titlebar-close" ).click()
# run the tests
_run_tests( control_tests, do_test, not pytest.config.option.short_tests ) #pylint: disable=no-member
# ---------------------------------------------------------------------
@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
def test_download_data( webapp, webdriver ):
"""Test downloading the data."""
# initialize
control_tests = init_webapp( webapp, webdriver, vlog_persistence=1, lfa_persistence=1 )
def do_test(): #pylint: disable=missing-docstring
# analyze the log file
_analyze_vlogs( "download-test.vlog" )
# download the data
marker = set_stored_msg_marker( "_lfa-download_" )
find_child( "#lfa button.download" ).click()
wait_for( 2, lambda: get_stored_msg("_lfa-download_") != marker )
data = get_stored_msg( "_lfa-download_" )
# check the results
data = data.split( "\n" )
rows = list( csv.reader( data, quoting=csv.QUOTE_NONNUMERIC ) )
assert rows == [
[ "Log file", "Phase", "Player", "Type", "Die 1", "Die 2" ],
[ "download-test.vlog", "", 'Joey "The Lips" Blow', "IFT", 4, 1 ],
[ "", "", 'Joey "The Lips" Blow', "IFT", 2, 5 ],
[ "", "", 'Joey "The Lips" Blow', "RS", 2, "" ],
[ "", "UN 1 PFPh", "\u65e5\u672c Guy", "IFT", 4, 6 ],
[ "", "", "\u65e5\u672c Guy", "IFT", 2, 6 ],
[ "", "", "\u65e5\u672c Guy", "RS", 3, "" ],
[ "", "UN 1 MPh", 'Joey "The Lips" Blow', "IFT", 2, 6 ],
[ "", "", 'Joey "The Lips" Blow', "IFT", 2, 3 ],
[ "", "", 'Joey "The Lips" Blow', "RS", 3, "" ]
]
# run the test
_run_tests( control_tests, do_test, False )
# ---------------------------------------------------------------------
def _analyze_vlogs( fnames ):
"""Analyze log file(s)."""
# initialize
if isinstance( fnames, str ):
fnames = [ fnames ]
select_menu_option( "analyze_vlog" )
dlg = wait_for_elem( 2, ".ui-dialog.lfa-upload" )
# add each log file
for fname in fnames:
fname = os.path.join( os.path.split(__file__)[0], "fixtures/analyze-vlog/"+fname )
vlog_data = open( fname, "rb" ).read()
set_stored_msg( "_vlog-persistence_", "{}|{}".format(
os.path.split( fname )[1],
base64.b64encode( vlog_data ).decode( "utf-8" )
) )
find_child( "button.add", dlg ).click()
wait_for( 2, lambda: get_stored_msg( "_vlog-persistence_" ) == "" )
# start the analysis
find_child( "button.ok", dlg ).click()
wait_for_elem( 30, "#lfa" )
def _get_chart_data( window_size=None ):
"""Unload the chart data from the page."""
# set the time-plot window size
if window_size is not None:
_set_time_plot_window_size( window_size )
# unload the chart data
remove_first_col = lambda data: [ row[1:] for row in data ]
remove_last_col = lambda data: [ row[:-1] for row in data ]
remove_first_row = lambda data: data[1:]
return {
"distrib": {
"dr": remove_first_col( remove_last_col( _unload_table( "distrib d6x1" ) ) ),
"DR": remove_first_col( remove_last_col( _unload_table( "distrib d6x2" ) ) ),
},
"pie": {
"dr": remove_first_row( _unload_table( "pie d6x1" ) ),
"DR": remove_first_row( _unload_table( "pie d6x2" ) ),
},
"timePlot": _unload_table( "time-plot" ),
"hotness": remove_first_row( _unload_table( "hotness" ) ),
}
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
def _select_roll_type( roll_type ):
"""Select the roll type."""
elem = find_child( "select[name='roll-type']" )
select_droplist_val( Select(elem), roll_type, isSelectMenu=True )
def _check_time_plot_window_sizes( expected ):
"""Check the available time-plot window sizes."""
elem = find_child( "select[name='moving-average']" )
vals = get_droplist_vals( Select(elem) )
assert [ int(v[0]) for v in vals ] == expected
def _set_time_plot_window_size( window_size ):
"""Select the specified time-plot moving average window size."""
elem = find_child( "select[name='moving-average']" )
select_droplist_val( Select(elem), window_size, isSelectMenu=True )
def _check_time_plot_values( expected_window_sizes, window_size, expected ):
"""Check the time-plot values."""
# set the window size
assert int(window_size) in expected_window_sizes
_set_time_plot_window_size( window_size )
# unload and check the time plot values
vals = _unload_table( "time-plot" )
assert vals == expected
def _unload_table( sel ):
"""Unload chart data from an HTML table."""
return unload_table(
"//*[@class='{}']//table[@class='chart-data']//tr".format( sel )
)

@ -221,7 +221,7 @@ def test_missing_templates( webapp, webdriver ):
def _make_zip( files ):
"""Generate a ZIP file."""
with TempFile() as temp_file:
temp_file.close()
temp_file.close( delete=False )
with zipfile.ZipFile( temp_file.name, "w" ) as zip_file:
for fname,fdata in files.items():
zip_file.writestr( fname, fdata )

@ -391,7 +391,7 @@ def _set_test_vasl_extn( control_tests, build_info, build_info_fname="buildFile"
with TempFile() as temp_file:
with zipfile.ZipFile( temp_file.name, "w" ) as zip_file:
zip_file.writestr( build_info_fname, build_info )
temp_file.close()
temp_file.close( delete=False )
with open( temp_file.name, "rb" ) as fp:
zip_data = fp.read()
control_tests.set_test_vasl_extn( fname="test.zip", bin_data=zip_data )

@ -146,7 +146,7 @@ def test_full_update( webapp, webdriver ):
with TempFile() as temp_file:
# check the results
temp_file.write( updated_vsav_data )
temp_file.close()
temp_file.close( delete=False )
updated_vsav_dump = _dump_vsav( temp_file.name )
expected = {
"scenario": "Modified scenario name (<>{}\"'\\)",
@ -862,7 +862,7 @@ def _update_vsav_and_dump( fname, expected ):
# dump the updated VSAV
with TempFile() as temp_file:
temp_file.write( updated_vsav_data )
temp_file.close()
temp_file.close( delete=False )
return _dump_vsav( temp_file.name )
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

@ -9,6 +9,7 @@ import typing
import uuid
from collections import defaultdict
import lxml.html
import pytest
from PyQt5.QtWidgets import QApplication
from selenium.webdriver.support.ui import Select
@ -480,22 +481,25 @@ def find_snippet_buttons( webdriver=None ):
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
def select_droplist_val( sel, val ):
def select_droplist_val( sel, val, isSelectMenu=False ):
"""Select a droplist option by value."""
_do_select_droplist( sel, val )
_do_select_droplist( sel, val, isSelectMenu )
def select_droplist_index( sel, index ):
def select_droplist_index( sel, index, isSelectMenu=False ):
"""Select a droplist option by index."""
options = get_droplist_vals( sel )
_do_select_droplist( sel, options[index][0] )
_do_select_droplist( sel, options[index][0], isSelectMenu )
def _do_select_droplist( sel, val ):
def _do_select_droplist( sel, val, isSelectMenu ):
"""Select a droplist option."""
sel_name = sel._el.get_attribute( "name" ) #pylint: disable=protected-access
_webdriver.execute_script(
"$(arguments[0]).val( '{}' ).trigger( 'change' )".format( val ),
find_child( "select[name='{}']".format( sel_name ) )
)
elem = find_child( "select[name='{}']".format( sel_name ) )
_webdriver.execute_script( "$(arguments[0]).val( '{}' )".format( val ), elem )
if isSelectMenu:
# NOTE: jQuery's selectmenu component requires a slightly different trigger
_webdriver.execute_script( "$(arguments[0]).trigger( 'selectmenuchange' ).selectmenu( 'refresh' )", elem )
else:
_webdriver.execute_script( "$(arguments[0]).val( '{}' ).trigger( 'change' )".format( val ), elem )
def get_droplist_vals_index( sel ):
"""Get the value/text for each option in a droplist."""
@ -606,6 +610,33 @@ def wait_for_clipboard( timeout, expected, contains=None, transform=None ):
# ---------------------------------------------------------------------
def unload_table( xpath ):
"""Unload data from an HTML table."""
# NOTE: Extracting table data using Selenium is extremely slow, we use lxml for the win!
def unload_cells( cells ):
"""Unload cell data from a table row."""
return [ "" if c.text is None else c.text.strip() for c in cells ]
# unload the table data
results = []
doc = lxml.html.fromstring( _webdriver.page_source )
for row in doc.xpath( xpath ):
if not results:
# we check for <th> in the first row only
cells = list( row.xpath( ".//th" ) )
if cells:
results.append( unload_cells( cells ) )
continue
# extract the next row
cells = row.xpath( ".//td" )
results.append( unload_cells( cells ) )
return results
# ---------------------------------------------------------------------
_IE_HTML_TAGS = [ "<i>" ]
def adjust_html( val ):

@ -1,6 +1,7 @@
""" Miscellaneous utilities. """
import os
import shutil
import io
import tempfile
import pathlib
@ -62,12 +63,13 @@ class TempFile:
self.temp_file = None
self.name = None
def __enter__( self ):
def open( self ):
"""Allocate a temp file."""
if self.encoding:
encoding = self.encoding
else:
encoding = "utf-8" if "b" not in self.mode else None
assert self.temp_file is None
self.temp_file = tempfile.NamedTemporaryFile(
mode = self.mode,
encoding = encoding,
@ -75,20 +77,32 @@ class TempFile:
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 close( self, delete ):
"""Close the temp file."""
self.temp_file.close()
if delete:
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()
def save_copy( self, fname, logger, caption ):
"""Make a copy of the temp file (for debugging porpoises)."""
if not fname:
return
logger.debug( "Saving a copy of the %s: %s", caption, fname )
shutil.copyfile( self.temp_file.name, fname )
def __enter__( self ):
"""Enter the context manager."""
self.open()
return self
def __exit__( self, exc_type, exc_val, exc_tb ):
"""Exit the context manager."""
self.close( delete=True )
# ---------------------------------------------------------------------

@ -51,7 +51,7 @@ def update_vsav(): #pylint: disable=too-many-statements,too-many-locals
# save the VSAV data in a temp file
input_file.write( vsav_data )
input_file.close()
input_file.close( delete=False )
fname = app.config.get( "UPDATE_VSAV_INPUT" ) # nb: for diagnosing problems
if fname:
logger.debug( "Saving a copy of the VSAV data: %s", fname )
@ -61,7 +61,7 @@ def update_vsav(): #pylint: disable=too-many-statements,too-many-locals
with TempFile() as snippets_file:
# save the snippets in a temp file
xml = _save_snippets( snippets, players, snippets_file, logger )
snippets_file.close()
snippets_file.close( delete=False )
fname = app.config.get( "UPDATE_VSAV_SNIPPETS" ) # nb: for diagnosing problems
if fname:
logger.debug( "Saving a copy of the snippets: %s", fname )
@ -70,8 +70,8 @@ def update_vsav(): #pylint: disable=too-many-statements,too-many-locals
# run the VASSAL shim to update the VSAV file
with TempFile() as output_file, TempFile() as report_file:
output_file.close()
report_file.close()
output_file.close( delete=False )
report_file.close( delete=False )
vassal_shim = VassalShim()
vassal_shim.update_scenario(
input_file.name, snippets_file.name, output_file.name, report_file.name
@ -82,7 +82,7 @@ def update_vsav(): #pylint: disable=too-many-statements,too-many-locals
fname = app.config.get( "UPDATE_VSAV_RESULT" ) # nb: for diagnosing problems
if fname:
logger.debug( "Saving a copy of the updated VSAV: %s", fname )
with open( app.config.get("UPDATE_VSAV_RESULT"), "wb" ) as fp:
with open( fname, "wb" ) as fp:
fp.write( vsav_data )
# read the report
report = _parse_label_report( report_file.name )
@ -221,7 +221,7 @@ def analyze_vsav():
# save the VSAV data in a temp file
input_file.write( vsav_data )
input_file.close()
input_file.close( delete=False )
fname = app.config.get( "ANALYZE_VSAV_INPUT" ) # nb: for diagnosing problems
if fname:
logger.debug( "Saving a copy of the VSAV data: %s", fname )
@ -230,7 +230,7 @@ def analyze_vsav():
# run the VASSAL shim to analyze the VSAV file
with TempFile() as report_file:
report_file.close()
report_file.close( delete=False )
vassal_shim = VassalShim()
vassal_shim.analyze_scenario( input_file.name, report_file.name )
report = _parse_analyze_report( report_file.name )
@ -307,7 +307,7 @@ class VassalShim:
return None
# FUDGE! We can't capture the output on Windows, get the result in a temp file instead :-/
with TempFile() as temp_file:
temp_file.close()
temp_file.close( delete=False )
VassalShim()._run_vassal_shim( "version", temp_file.name ) #pylint: disable=protected-access
with open( temp_file.name, "r" ) as fp:
return fp.read()
@ -319,7 +319,7 @@ class VassalShim:
def analyze_scenario( self, vsav_fname, report_fname ):
"""Analyze a scenario file."""
return self._run_vassal_shim(
"analyze", VassalShim.get_boards_dir(), vsav_fname, report_fname
"analyze", vsav_fname, report_fname
)
def update_scenario( self, vsav_fname, snippets_fname, output_fname, report_fname ):
@ -330,6 +330,12 @@ class VassalShim:
"update", VassalShim.get_boards_dir(), vsav_fname, snippets_fname, output_fname, report_fname
)
def analyze_logfiles( self, *fnames ):
"""Analyze a log file."""
return self._run_vassal_shim(
"analyzeLogs", *fnames
)
def _run_vassal_shim( self, *args ): #pylint: disable=too-many-locals
"""Run the VASSAL shim."""
@ -352,7 +358,7 @@ class VassalShim:
java_path, "-classpath", class_path, "vassal_shim.Main",
args[0]
]
if args[0] in ("dump","analyze","update"):
if args[0] in ("dump","analyze","analyzeLogs","update"):
if not globvars.vasl_mod:
raise SimpleError( "The VASL module has not been configured." )
args2.append( globvars.vasl_mod.filename )
@ -394,9 +400,9 @@ class VassalShim:
except subprocess.TimeoutExpired:
proc.kill()
raise
buf1.close()
buf1.close( delete=False )
stdout = open( buf1.name, "r", encoding="utf-8" ).read()
buf2.close()
buf2.close( delete=False )
stderr = open( buf2.name, "r", encoding="utf-8" ).read()
elapsed_time = time.time() - start_time
logger.info( "- Completed OK: %.3fs", elapsed_time )

@ -123,11 +123,11 @@ class WebDriver:
# 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()
html_tempfile.close( delete=False )
self.driver.get( "file://{}".format( html_tempfile.name ) )
# take a screenshot of the HTML
screenshot_tempfile.close()
screenshot_tempfile.close( delete=False )
self.driver.set_window_size( window_size[0], window_size[1] )
img = do_get_screenshot( screenshot_tempfile.name )
retry_ratio = float( app.config.get( "WEBDRIVER_SCREENSHOT_RETRY_RATIO", 0.8 ) )

@ -2,6 +2,7 @@ package vassal_shim ;
import java.io.BufferedWriter ;
import java.io.FileWriter ;
import java.util.ArrayList ;
import VASSAL.Info ;
@ -23,25 +24,34 @@ public class Main
try {
String cmd = args[0].toLowerCase() ;
if ( cmd.equals( "dump" ) ) {
checkArgs( args, 3, "the VASL .vmod file and scenario file" ) ;
checkArgs( args, 3, false, "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( "analyze" ) ) {
checkArgs( args, 5, "the VASL .vmod file, boards directory, scenario file and output file" ) ;
VassalShim shim = new VassalShim( args[1], args[2] ) ;
shim.analyzeScenario( args[3], args[4] ) ;
checkArgs( args, 4, false, "the VASL .vmod file, scenario file and output file" ) ;
VassalShim shim = new VassalShim( args[1], null ) ;
shim.analyzeScenario( args[2], args[3] ) ;
System.exit( 0 ) ;
}
else if ( cmd.equals( "analyzelogs" ) ) {
checkArgs( args, 4, true, "the VASL .vmod file, log file(s) and output file" ) ;
ArrayList<String> logFilenames = new ArrayList<String>() ;
for ( int i=2 ; i < args.length-1 ; ++i )
logFilenames.add( args[i] ) ;
VassalShim shim = new VassalShim( args[1], null ) ;
shim.analyzeLogs( logFilenames, args[args.length-1] ) ;
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" ) ;
checkArgs( args, 7, false, "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 if ( cmd.equals( "version" ) ) {
checkArgs( args, 2, "the output file" ) ;
checkArgs( args, 2, false, "the output file" ) ;
System.out.println( Info.getVersion() ) ;
// FUDGE! The Python web server can't capture output on Windows - save the result to a file as well :-/
BufferedWriter writer = new BufferedWriter( new FileWriter( args[1] ) ) ;
@ -60,10 +70,15 @@ public class Main
}
}
private static void checkArgs( String[]args, int expected, String hint )
private static void checkArgs( String[] args, int expected, boolean orMore, String hint )
{
// check the number of arguments
if ( args.length != expected ) {
boolean ok ;
if ( orMore )
ok = args.length >= expected ;
else
ok = args.length == expected ;
if ( ! ok ) {
System.out.println( "Incorrect number of arguments, please specify " + hint + "." ) ;
System.exit( 2 ) ;
}

@ -9,6 +9,7 @@ import javax.xml.transform.TransformerException ;
import javax.xml.transform.TransformerConfigurationException ;
import javax.xml.transform.dom.DOMSource ;
import javax.xml.transform.stream.StreamResult ;
import java.util.regex.Matcher ;
import org.w3c.dom.Document ;
import org.w3c.dom.NodeList ;
@ -44,6 +45,16 @@ public class Utils
return buf.toString() ;
}
public static String getCapturedGroup( Matcher matcher, String groupName, String defaultVal )
{
// get the captured group
try {
return matcher.group( groupName ) ;
} catch( IllegalArgumentException exc ) {
return defaultVal ;
}
}
public static boolean startsWith( String val, String target )
{
// check if a string starts with a target substring

@ -44,9 +44,11 @@ import VASSAL.build.GameModule ;
import VASSAL.build.GpIdChecker ;
import VASSAL.build.module.GameState ;
import VASSAL.build.module.GameComponent ;
import VASSAL.build.module.BasicLogger.LogCommand ;
import VASSAL.build.module.ModuleExtension ;
import VASSAL.build.module.ObscurableOptions ;
import VASSAL.build.module.metadata.SaveMetaData ;
import VASSAL.build.module.Chatter.DisplayText ;
import VASSAL.build.widget.PieceSlot ;
import VASSAL.launch.BasicModule ;
import VASSAL.command.Command ;
@ -70,14 +72,7 @@ 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.AnalyzeNode ;
import vassal_shim.ModuleManagerMenuManager ;
import vassal_shim.AppBoolean ;
import vassal_shim.Utils ;
import vassal_shim.lfa.* ;
// --------------------------------------------------------------------
@ -144,11 +139,144 @@ public class VassalShim
dumpCommand( cmd, "" ) ;
}
public void analyzeLogs( ArrayList<String> logFilenames, String reportFilename )
throws IOException, TransformerException, TransformerConfigurationException, ParserConfigurationException
{
// analyze each log file
ArrayList<LogFileAnalysis> results = new ArrayList<LogFileAnalysis>() ;
for ( String fname: logFilenames )
results.add( this._doAnalyzeLogs( fname ) ) ;
// generate the report
Document doc = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument() ;
Element rootElem = doc.createElement( "logFileAnalysis" ) ;
doc.appendChild( rootElem ) ;
for ( LogFileAnalysis logFileAnalysis: results ) {
// add a node for the next log file
Element logFileElem = doc.createElement( "logFile" ) ;
logFileElem.setAttribute( "filename", logFileAnalysis.logFilename ) ;
// check if we found a scenario name
if ( logFileAnalysis.scenarioName.length() > 0 ) {
Element scenarioElem = doc.createElement( "scenario" ) ;
if ( logFileAnalysis.scenarioId.length() > 0 )
scenarioElem.setAttribute( "id", logFileAnalysis.scenarioId ) ;
scenarioElem.setTextContent( logFileAnalysis.scenarioName ) ;
logFileElem.appendChild( scenarioElem ) ;
}
// add the extracted events
Element eventsElems = doc.createElement( "events" ) ;
logFileElem.appendChild( eventsElems ) ;
for ( Event evt: logFileAnalysis.events )
eventsElems.appendChild( evt.makeXmlElement( doc ) ) ;
rootElem.appendChild( logFileElem ) ;
}
Utils.saveXml( doc, reportFilename ) ;
}
public LogFileAnalysis _doAnalyzeLogs( String logFilename )
throws IOException, TransformerException, TransformerConfigurationException, ParserConfigurationException
{
// load the log file
Command cmd = loadScenario( logFilename ) ;
// extract events
ArrayList<Event> events = new ArrayList<Event>() ;
findLogFileEvents( cmd, events ) ;
// extract the scenario details
String scenarioName="", scenarioId="" ;
String[] players = new String[2] ;
Map<String,GamePieceLabelFields> ourLabels = new HashMap<String,GamePieceLabelFields>() ;
ArrayList<GamePieceLabelFields> otherLabels = new ArrayList<GamePieceLabelFields>() ;
AppBoolean hasPlayerOwnedLabels = new AppBoolean( false ) ;
extractLabels( cmd, players, hasPlayerOwnedLabels, ourLabels, otherLabels, false ) ;
GamePieceLabelFields labelFields = ourLabels.get( "scenario" ) ;
if ( labelFields != null ) {
Matcher matcher = Pattern.compile( "<span class=\"scenario-name\">(.*?)</span>" ).matcher(
labelFields.getLabelContent()
) ;
if ( matcher.find() )
scenarioName = matcher.group( 1 ).trim() ;
matcher = Pattern.compile( "<span class=\"scenario-id\">(.*?)</span>" ).matcher(
labelFields.getLabelContent()
) ;
if ( matcher.find() ) {
scenarioId = matcher.group( 1 ).trim() ;
if ( scenarioId.length() > 0 && scenarioId.charAt(0) == '(' && scenarioId.charAt(scenarioId.length()-1) == ')' )
scenarioId = scenarioId.substring( 1, scenarioId.length()-1 ) ;
}
}
return new LogFileAnalysis( logFilename, scenarioName, scenarioId, events ) ;
}
private void findLogFileEvents( Command cmd, ArrayList<Event> events )
{
// NOTE: VASSAL doesn't store die/dice rolls and Turn Track rotations as specific events in the log file,
// but as plain-text messages in the chat window (which sorta makes sense, since VASSAL won't really understand
// these things, since they are specific to the game module).
// initialize
Pattern diceRollEventPattern = Pattern.compile(
config.getProperty( "LFA_PATTERN_DICE_ROLL", "^\\*\\*\\* \\((?<rollType>.+?) (?<drType>DR|dr)\\) (?<values>.+?) \\*\\*\\*\\s+\\<(?<player>.+?)\\>" )
) ;
Pattern diceRoll3EventPattern = Pattern.compile(
config.getProperty( "LFA_PATTERN_DICE3_ROLL", "^\\*\\*\\* 3d6 = (?<d1>\\d),(?<d2>\\d),(?<d3>\\d) \\*\\*\\*\\s+\\<(?<player>.+?)\\>" )
) ;
Pattern turnTrackEventPatter = Pattern.compile(
config.getProperty( "LFA_PATTERN_TURN_TRACK", "^\\* New: (?<side>.+?) Turn (?<turn>\\d+) - (?<phase>.+?) \\*" )
) ;
// check if we've found an event we're interested in
if ( cmd instanceof LogCommand ) {
LogCommand logCmd = (LogCommand) cmd ;
if ( logCmd.getLoggedCommand() instanceof DisplayText ) {
String msg = ((DisplayText)logCmd.getLoggedCommand()).getMessage() ;
logger.debug( "Found display text: " + msg ) ;
Event event = null ;
// check if we've found a DR/dr roll
Matcher matcher = diceRollEventPattern.matcher( msg ) ;
if ( matcher.find() ) {
// yup - add it to the list
String rollType = Utils.getCapturedGroup( matcher, "rollType", "Other" ) ;
event = new DiceEvent( matcher.group("player"), rollType, matcher.group("values") ) ;
logger.debug( "- Matched DiceEvent: " + event ) ;
}
// check if we've found a 3d6 roll
if ( event == null ) {
matcher = diceRoll3EventPattern.matcher( msg ) ;
if ( matcher.find() ) {
// yup - add it to the list as two separate events (DR and dr)
Event event0 = new DiceEvent( matcher.group("player"), "3d6 (DR)", matcher.group("d1")+","+matcher.group("d2") ) ;
events.add( event0 ) ;
event = new DiceEvent( matcher.group("player"), "3d6 (dr)", matcher.group("d3") ) ;
logger.debug( "- Matched 3d6 DiceEvent's: " + event0 + " ; " + event ) ;
}
}
// check if we've found a Turn Track change
if ( event == null ) {
matcher = turnTrackEventPatter.matcher( msg ) ;
if ( matcher.find() ) {
// yup - add it to the list
event = new TurnTrackEvent( matcher.group("side"), matcher.group("turn"), matcher.group("phase") ) ;
logger.debug( "- Matched TurnTrackEvent: " + event ) ;
}
}
// add the event to the list
if ( event != null )
events.add( event ) ;
}
}
// check any child commands
for ( Command c: cmd.getSubCommands() )
findLogFileEvents( c, events ) ;
}
public void analyzeScenario( String scenarioFilename, String reportFilename )
throws IOException, ParserConfigurationException, TransformerConfigurationException, TransformerException
{
// load the scenario
configureBoards() ;
Command cmd = loadScenario( scenarioFilename ) ;
cmd.execute() ;
@ -219,7 +347,7 @@ public class VassalShim
ArrayList<GamePieceLabelFields> otherLabels = new ArrayList<GamePieceLabelFields>() ;
logger.info( "Searching the VASL scenario for labels (players={};{})...", players[0], players[1] ) ;
AppBoolean hasPlayerOwnedLabels = new AppBoolean( false ) ;
extractLabels( cmd, players, hasPlayerOwnedLabels, ourLabels, otherLabels ) ;
extractLabels( cmd, players, hasPlayerOwnedLabels, ourLabels, otherLabels, true ) ;
// NOTE: vasl-templates v1.2 started tagging labels with their owning player e.g. "germans/ob_setup_1.1".
// This is so that we can ignore labels owned by nationalities not directly involved in the scenario.
@ -299,7 +427,7 @@ public class VassalShim
}
}
private void extractLabels( Command cmd, String[] players, AppBoolean hasPlayerOwnedLabels, Map<String,GamePieceLabelFields> ourLabels, ArrayList<GamePieceLabelFields> otherLabels )
private void extractLabels( Command cmd, String[] players, AppBoolean hasPlayerOwnedLabels, Map<String,GamePieceLabelFields> ourLabels, ArrayList<GamePieceLabelFields> otherLabels, boolean legacyMode )
{
// check if this command is a label we're interested in
// NOTE: We shouldn't really be looking at the object type, see analyzeScenario().
@ -311,9 +439,17 @@ public class VassalShim
if ( gamePiece.getName().equals( "User-Labeled" ) ) {
// yup - parse the label content
// FUDGE! This method gets called as part of log file analysis, but it wasn't finding the scenario label,
// because we were calling getState() on the target GamePiece instead of its parent AddPiece (as
// the "dump" command does). Simply changing this behavior caused some tests to fail, so since updating
// scenarios is a critical (and potentially dangerous) operation, we maintain the old and proven code, and
// switch to the new code only when requested.
ArrayList<String> separators = new ArrayList<String>() ;
ArrayList<String> fields = new ArrayList<String>() ;
parseGamePieceState( target.getState(), separators, fields ) ;
if ( legacyMode )
parseGamePieceState( target.getState(), separators, fields ) ;
else
parseGamePieceState( addPieceCmd.getState(), separators, fields ) ;
// check if the label is one of ours
String snippetId = isVaslTemplatesLabel( fields, GamePieceLabelFields.FIELD_INDEX_LABEL1 ) ;
@ -370,7 +506,7 @@ public class VassalShim
// extract labels in sub-commands
for ( Command c: cmd.getSubCommands() )
extractLabels( c, players, hasPlayerOwnedLabels, ourLabels, otherLabels ) ;
extractLabels( c, players, hasPlayerOwnedLabels, ourLabels, otherLabels, legacyMode ) ;
}
private String isVaslTemplatesLabel( ArrayList<String> fields, int fieldIndex )
@ -894,6 +1030,8 @@ public class VassalShim
dumpCommandExtras( (GameState.SetupCommand)cmd, buf, prefix ) ;
else if ( cmd instanceof ModuleExtension.RegCmd )
dumpCommandExtras( (ModuleExtension.RegCmd)cmd, buf, prefix ) ;
else if ( cmd instanceof LogCommand )
dumpCommandExtras( (LogCommand)cmd, buf, prefix ) ;
else if ( cmd instanceof ObscurableOptions.SetAllowed )
dumpCommandExtras( (ObscurableOptions.SetAllowed)cmd, buf, prefix ) ;
System.out.println( buf.toString() ) ;
@ -945,6 +1083,12 @@ public class VassalShim
buf.append( ": " + cmd.getName() + " (" + cmd.getVersion() + ")" ) ;
}
private static void dumpCommandExtras( LogCommand cmd, StringBuilder buf, String prefix )
{
// dump extra command info
buf.append( ": " + cmd.getLoggedCommand() ) ;
}
private static void dumpCommandExtras( ObscurableOptions.SetAllowed cmd, StringBuilder buf, String prefix )
{
// dump extra command info

@ -0,0 +1,37 @@
package vassal_shim.lfa ;
import org.w3c.dom.Document ;
import org.w3c.dom.Element ;
// --------------------------------------------------------------------
public class DiceEvent implements Event
{
String playerName ;
String rollType ;
String rollValues ;
public DiceEvent( String playerName, String rollType, String rollValues )
{
// initialize the DiceEvent
this.playerName = playerName ;
this.rollType = rollType ;
this.rollValues = rollValues ;
}
public Element makeXmlElement( Document doc )
{
// create an XML element for the DiceEvent
Element elem = doc.createElement( "diceEvent" ) ;
elem.setAttribute( "player", playerName ) ;
elem.setAttribute( "rollType", rollType ) ;
elem.setTextContent( rollValues ) ;
return elem ;
}
public String toString()
{
// return the DiceEvent as a string
return "<DiceEvent:" + playerName + ":" + rollType + ":" + rollValues + ">" ;
}
}

@ -0,0 +1,12 @@
package vassal_shim.lfa ;
import org.w3c.dom.Document ;
import org.w3c.dom.Element ;
// --------------------------------------------------------------------
public interface Event
{
public Element makeXmlElement( Document doc ) ;
}

@ -0,0 +1,20 @@
package vassal_shim.lfa ;
import java.util.ArrayList ;
// --------------------------------------------------------------------
public class LogFileAnalysis
{
public String logFilename ;
public String scenarioName ;
public String scenarioId ;
public ArrayList<Event> events ;
public LogFileAnalysis( String logFilename, String scenarioName, String scenarioId, ArrayList<Event> events ) {
this.logFilename = logFilename ;
this.scenarioName = scenarioName ;
this.scenarioId = scenarioId ;
this.events = events ;
}
}

@ -0,0 +1,37 @@
package vassal_shim.lfa ;
import org.w3c.dom.Document ;
import org.w3c.dom.Element ;
// --------------------------------------------------------------------
public class TurnTrackEvent implements Event
{
String playerSide ;
String turnNo ;
String phaseName ;
public TurnTrackEvent( String playerSide, String turnNo, String phaseName )
{
// initialize the TurnTrackEvent
this.playerSide = playerSide ;
this.turnNo = turnNo ;
this.phaseName = phaseName ;
}
public Element makeXmlElement( Document doc )
{
// create an XML element for the TurnTrackEvent
Element elem = doc.createElement( "turnTrackEvent" ) ;
elem.setAttribute( "side", playerSide ) ;
elem.setAttribute( "turnNo", turnNo ) ;
elem.setAttribute( "phase", phaseName ) ;
return elem ;
}
public String toString()
{
// return the TurnTrackEvent as a string
return "<TurnTrackEvent:" + playerSide + ":" + turnNo + ":" + phaseName + ">" ;
}
}
Loading…
Cancel
Save