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 os
import re import re
import json import json
import io
import base64 import base64
import logging import logging
@ -87,7 +86,7 @@ class MainWindow( QWidget ):
self.restoreGeometry( val ) self.restoreGeometry( val )
else : else :
self.resize( 1050, 650 ) self.resize( 1050, 650 )
self.setMinimumSize( 1000, 595 ) self.setMinimumSize( 1050, 620 )
# initialize the layout # initialize the layout
layout = QVBoxLayout( self ) layout = QVBoxLayout( self )
@ -215,27 +214,29 @@ class MainWindow( QWidget ):
NOTE: This handler might be called multiple times. 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 # load and install the user settings
buf = io.StringIO() user_settings = {}
buf.write( "{" )
for key in app_settings.allKeys(): for key in app_settings.allKeys():
if key.startswith( "UserSettings/" ): if key.startswith( "UserSettings/" ):
val = app_settings.value(key) val = app_settings.value( key )
if val in ("true","false") or val.isdigit(): key = key[13:] # remove the leading "UserSettings/"
buf.write( '"{}": {},'.format( key[13:], val ) ) sections = key.split( "." )
else: target = user_settings
buf.write( '"{}": "{}",'.format( key[13:], val ) ) while len(sections) > 1:
buf.write( '"_dummy_": null }' ) if sections[0] not in target:
buf = buf.getvalue() target[ sections[0] ] = {}
user_settings = {} target = target[ sections.pop(0) ]
try: target[ sections[0] ] = decode_val( val )
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_"]
self._view.page().runJavaScript( self._view.page().runJavaScript(
"install_user_settings('{}')".format( json.dumps( user_settings ) ) "install_user_settings('{}')".format( json.dumps( user_settings ) )
) )
@ -284,6 +285,12 @@ class MainWindow( QWidget ):
data = base64.b64decode( data ) data = base64.b64decode( data )
return self._web_channel_handler.save_updated_vsav( fname, 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 ) @pyqtSlot( str )
@catch_exceptions( caption="SLOT EXCEPTION" ) @catch_exceptions( caption="SLOT EXCEPTION" )
def on_user_settings_change( self, user_settings ): #pylint: disable=no-self-use def on_user_settings_change( self, user_settings ): #pylint: disable=no-self-use
@ -293,9 +300,22 @@ class MainWindow( QWidget ):
if key.startswith( "UserSettings/" ): if key.startswith( "UserSettings/" ):
app_settings.remove( key ) app_settings.remove( key )
# save the new user settings # save the new user settings
user_settings = json.loads( user_settings ) def save_section( vals, key_prefix ):
for key,val in user_settings.items(): """Save a section of the User Settings."""
app_settings.setValue( "UserSettings/{}".format(key), val ) 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 ) @pyqtSlot( str, bool )
@catch_exceptions( caption="SLOT EXCEPTION" ) @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 (*)", "VASL scenario files (*.vsav);;All files (*)",
"scenario.vsav" "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 ): def on_new_scenario( self ):
"""Called when the scenario is reset.""" """Called when the scenario is reset."""
@ -82,3 +88,12 @@ class WebChannelHandler:
dname = os.path.split( self.updated_vsav_file_dialog.curr_fname )[0] dname = os.path.split( self.updated_vsav_file_dialog.curr_fname )[0]
self.updated_vsav_file_dialog.curr_fname = os.path.join( dname, fname ) self.updated_vsav_file_dialog.curr_fname = os.path.join( dname, fname )
return self.updated_vsav_file_dialog.save_file( data ) 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.vo_notes #pylint: disable=cyclic-import
import vasl_templates.webapp.nat_caps #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.roar #pylint: disable=cyclic-import
import vasl_templates.webapp.lfa #pylint: disable=cyclic-import
if app.config.get( "ENABLE_REMOTE_TEST_CONTROL" ): if app.config.get( "ENABLE_REMOTE_TEST_CONTROL" ):
print( "*** WARNING: Remote test control enabled! ***" ) print( "*** WARNING: Remote test control enabled! ***" )
import vasl_templates.webapp.testing #pylint: disable=cyclic-import import vasl_templates.webapp.testing #pylint: disable=cyclic-import

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

@ -1,5 +1,5 @@
<html> <!-- vasl-templates:id {{SNIPPET_ID}} --> <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}} Time: {{TIMESTAMP}}
VASSAL:{%if VASSAL_VERSION %} {{VASSAL_VERSION}} {%else%} - {%endif%} VASSAL:{%if VASSAL_VERSION %} {{VASSAL_VERSION}} {%else%} - {%endif%}
VASL: {%if VASL_VERSION%} {{VASL_VERSION}} {%else%} - {%endif%} VASL: {%if VASL_VERSION%} {{VASL_VERSION}} {%else%} - {%endif%}
@ -9,7 +9,9 @@
<meta charset="utf-8"> <meta charset="utf-8">
<style> <style>
{{CSS:common}} {{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> </style>
</head> </head>
@ -24,12 +26,12 @@
border-bottom: 1px solid #c0c0c0 ; border-bottom: 1px solid #c0c0c0 ;
"> ">
<div style="font-size:115%;"> <div style="font-size:115%;">
<b> {%if SCENARIO_NAME%} {{SCENARIO_NAME}} {%else%} Untitled scenario {%endif%} </b> <span class="scenario-name"> {%if SCENARIO_NAME%} {{SCENARIO_NAME}} {%else%} Untitled scenario {%endif%} </span>
{%if SCENARIO_ID%} <span class="small"> ({{SCENARIO_ID}}) </span> {%endif%} {%if SCENARIO_ID%} <span class="scenario-id"> ({{SCENARIO_ID}}) </span> {%endif%}
</div> </div>
{%if SCENARIO_LOCATION%} {{SCENARIO_LOCATION}} {%endif%} {%if SCENARIO_LOCATION%} <span class="location"> {{SCENARIO_LOCATION}} </span> {%endif%}
{%if SCENARIO_DATE%} {%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%} {%endif%}
</table> </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 import vasl_templates.webapp.config.constants
from vasl_templates.webapp.config.constants import BASE_DIR, DATA_DIR from vasl_templates.webapp.config.constants import BASE_DIR, DATA_DIR
from vasl_templates.webapp import globvars 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). # 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 INSTANCE_ID = uuid.uuid4().hex
@ -72,6 +73,17 @@ _APP_CONFIG_DEFAULTS = { # Bodhgaya, India (APR/19)
def get_app_config(): def get_app_config():
"""Get the application 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 # include the basic app config
vals = { vals = {
key: app.config.get( key, default ) 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"]: for key in ["APP_NAME","APP_VERSION","APP_DESCRIPTION","APP_HOME_URL"]:
vals[ key ] = getattr( vasl_templates.webapp.config.constants, key ) 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 # 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 # 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 # 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 .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 { 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 fieldset { margin: 0.5em 0 0 0 ; padding-top: 0.5em ; border-radius: 0 ; }
.ui-dialog.user-settings .select2 { margin-top: -4px ; } .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" }, separator: { type: "separator" },
analyze_vsav: { label: "Analyze VASL scenario", action: on_analyze_vsav }, analyze_vsav: { label: "Analyze VASL scenario", action: on_analyze_vsav },
update_vsav: { label: "Update VASL scenario", action: on_update_vsav }, update_vsav: { label: "Update VASL scenario", action: on_update_vsav },
analyze_vlog: { label: "Analyze log files", action: on_analyze_vlog },
separator2: { type: "separator" }, separator2: { type: "separator" },
template_pack: { label: "Load template pack", action: on_template_pack }, template_pack: { label: "Load template pack", action: on_template_pack },
user_settings: { label: "Settings", action: user_settings }, user_settings: { label: "Settings", action: user_settings },
@ -84,6 +85,7 @@ $(document).ready( function () {
$("#load-scenario").change( on_load_scenario_file_selected ) ; $("#load-scenario").change( on_load_scenario_file_selected ) ;
$("#load-template-pack").change( on_template_pack_file_selected ) ; $("#load-template-pack").change( on_template_pack_file_selected ) ;
$("#load-vsav").change( on_load_vsav_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 // all done - we can show the menu now
$("#menu").show() ; $("#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 // figure out how many pixels an em is
var $em = $( "<span>M</span>" ) ; var $em = $( "<span>M</span>" ) ;
$("body").append( $em ) ; $("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 } ; gUserSettings = Cookies.getJSON( "user-settings" ) || { "scenario-images-source": SCENARIO_IMAGES_SOURCE_INTERNET } ;
USER_SETTINGS = { USER_SETTINGS = {
"vasl-username": "text",
"snippet-font-family": "text", "snippet-font-family": "text",
"snippet-font-size": "text", "snippet-font-size": "text",
"date-format": "droplist", "date-format": "droplist",
@ -77,7 +78,7 @@ function user_settings()
dialogClass: "user-settings", dialogClass: "user-settings",
modal: true, modal: true,
width: 460, width: 460,
height: 350, height: 375,
resizable: false, resizable: false,
create: function() { create: function() {
init_dialog( $(this), "OK", true ) ; init_dialog( $(this), "OK", true ) ;
@ -114,12 +115,9 @@ function user_settings()
buttons: { buttons: {
OK: function() { OK: function() {
// unload and install the new user settings // unload and install the new user settings
var settings = unload_settings() ; gUserSettings = unload_settings() ;
gUserSettings = settings ; save_user_settings() ;
Cookies.set( "user-settings", settings, { expires: 999 } ) ;
apply_user_settings() ; apply_user_settings() ;
if ( gWebChannelHandler )
gWebChannelHandler.on_user_settings_change( JSON.stringify( settings ) ) ;
$(this).dialog( "close" ) ; $(this).dialog( "close" ) ;
}, },
Cancel: function() { $(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() function apply_user_settings()
{ {
// set the date format // 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 ) function fixup_external_links( $root )
{ {
// NOTE: We want to open externals links in a new browser window, but simply adding target="_blank" // 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 ; return null ;
} }
function stopEvent( evt )
{
// stop further processing for the event
evt.preventDefault() ;
evt.stopPropagation() ;
}
function isIE() function isIE()
{ {
// check if we're running in IE :-/ // check if we're running in IE :-/

@ -22,9 +22,9 @@ function _do_update_vsav( vsav_data, fname )
type: "POST", type: "POST",
data: JSON.stringify( data ), data: JSON.stringify( data ),
contentType: "application/json", contentType: "application/json",
} ).done( function( data ) { } ).done( function( resp ) {
$dlg.dialog( "close" ) ; $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 ) if ( ! data )
return ; return ;
// check if anything was changed // check if anything was changed
@ -322,9 +322,9 @@ function _do_analyze_vsav( vsav_data, fname )
type: "POST", type: "POST",
data: JSON.stringify( data ), data: JSON.stringify( data ),
contentType: "application/json", contentType: "application/json",
} ).done( function( data ) { } ).done( function( resp ) {
$dlg.dialog( "close" ) ; $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 ) if ( ! data )
return ; return ;
_create_vo_entries_from_analysis( data ) ; _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 // check if there was an error
if ( ! data.error ) if ( ! resp.error )
return data ; return resp ;
// yup - report the error // yup - report the error
if ( getUrlParam( "vsav_persistence" ) ) { if ( getUrlParam( "vsav_persistence" ) ) {
$( "#_vsav-persistence_" ).val( $( "#_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 ; 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( { $( "#vassal-shim-error" ).dialog( {
dialogClass: "vassal-shim-error", dialogClass: "vassal-shim-error",
title: caption, title: caption,
modal: true, modal: true,
width: 600, height: "auto", width: 600, height: "auto",
open: function() { open: function() {
$( "#vassal-shim-error .message" ).html( data.error ) ; $( "#vassal-shim-error .message" ).html( resp.error ) ;
var log = "" ; var log = "" ;
if ( data.stdout && data.stderr ) if ( resp.stdout && resp.stderr )
log = "=== STDOUT ===\n" + data.stdout + "\n=== STDERR ===\n" + data.stderr ; log = "=== STDOUT ===\n" + resp.stdout + "\n=== STDERR ===\n" + resp.stderr ;
else if ( data.stdout ) else if ( resp.stdout )
log = data.stdout ; log = resp.stdout ;
else if ( data.stderr ) else if ( resp.stderr )
log = data.stderr ; log = resp.stderr ;
if ( log ) if ( log )
$( "#vassal-shim-error .log" ).val( log ).show() ; $( "#vassal-shim-error .log" ).val( log ).show() ;
else else
@ -515,8 +525,6 @@ function _check_vassal_shim_response( data, caption )
Close: function() { $(this).dialog( "close" ) ; }, 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='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='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='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='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/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.css')}}" />
<link rel="stylesheet" type="text/css" href="{{url_for('static',filename='css/tabs-scenario.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/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/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/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/snippets.css')}}" />
<link rel="stylesheet" type="text/css" href="{{url_for('static',filename='css/user-settings-dialog.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;"> <div id="menu" style="display:none;">
<input type="image" src="{{url_for('static',filename='images/menu.png')}}" value="actions"> <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> </div>
{%include "tabs.html"%} {%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> <div id="alt-webapp-base-url" style="display:none;"> Webapp base URL: </div>
<!-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --> <!-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -->
@ -69,6 +75,8 @@
{%include "select-roar-scenario-dialog.html"%} {%include "select-roar-scenario-dialog.html"%}
{%include "vassal.html"%} {%include "vassal.html"%}
{%include "lfa.html"%}
{%include "lfa-upload.html"%}
{%include "snippets.html"%} {%include "snippets.html"%}
{%include "user-settings-dialog.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='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='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='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='download/download.min.js')}}"></script>
<script src="{{url_for('static',filename='jszip/jszip.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='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='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> <script>
gAppName = "{{APP_NAME}}" ; gAppName = "{{APP_NAME}}" ;
@ -105,6 +117,7 @@ gOrdnanceNotesUrl = "{{url_for('get_ordnance_notes')}}" ;
gGetVaslPieceInfoUrl = "{{url_for('get_vasl_piece_info')}}" ; gGetVaslPieceInfoUrl = "{{url_for('get_vasl_piece_info')}}" ;
gGetRoarScenarioIndexUrl = "{{url_for('get_roar_scenario_index')}}" ; gGetRoarScenarioIndexUrl = "{{url_for('get_roar_scenario_index')}}" ;
gAnalyzeVsavUrl = "{{url_for('analyze_vsav')}}" ; gAnalyzeVsavUrl = "{{url_for('analyze_vsav')}}" ;
gAnalyzeVlogsUrl = "{{url_for('analyze_vlogs')}}" ;
gUpdateVsavUrl = "{{url_for('update_vsav')}}" ; gUpdateVsavUrl = "{{url_for('update_vsav')}}" ;
gMakeSnippetImageUrl = "{{url_for('make_snippet_image')}}" ; gMakeSnippetImageUrl = "{{url_for('make_snippet_image')}}" ;
gHelpUrl = "{{url_for('show_help')}}" ; 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='vo.js')}}"></script>
<script src="{{url_for('static',filename='vo2.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='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='roar.js')}}"></script>
<script src="{{url_for('static',filename='sortable.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='user_settings.js')}}"></script>
<script src="{{url_for('static',filename='jQueryHandlers.js')}}"></script>
<script src="{{url_for('static',filename='utils.js')}}"></script> <script src="{{url_for('static',filename='utils.js')}}"></script>
{%include "testing.html"%} {%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="_template-pack-persistence_" style="display:none;"></textarea>
<textarea id="_scenario-persistence_" style="display:none;"></textarea> <textarea id="_scenario-persistence_" style="display:none;"></textarea>
<textarea id="_vsav-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> <textarea id="_snippet-image-persistence_" style="display:none;"></textarea>
<button id="popmenu-hack" style="display:none;position:absolute;top:0;left:0;"> <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 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;"> <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-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")'> <input type="text" name="snippet-font-size" size="3" title='CSS font size (e.g. "12px")'>
</div> </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 ): def _make_zip( files ):
"""Generate a ZIP file.""" """Generate a ZIP file."""
with TempFile() as temp_file: with TempFile() as temp_file:
temp_file.close() temp_file.close( delete=False )
with zipfile.ZipFile( temp_file.name, "w" ) as zip_file: with zipfile.ZipFile( temp_file.name, "w" ) as zip_file:
for fname,fdata in files.items(): for fname,fdata in files.items():
zip_file.writestr( fname, fdata ) 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 TempFile() as temp_file:
with zipfile.ZipFile( temp_file.name, "w" ) as zip_file: with zipfile.ZipFile( temp_file.name, "w" ) as zip_file:
zip_file.writestr( build_info_fname, build_info ) zip_file.writestr( build_info_fname, build_info )
temp_file.close() temp_file.close( delete=False )
with open( temp_file.name, "rb" ) as fp: with open( temp_file.name, "rb" ) as fp:
zip_data = fp.read() zip_data = fp.read()
control_tests.set_test_vasl_extn( fname="test.zip", bin_data=zip_data ) 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: with TempFile() as temp_file:
# check the results # check the results
temp_file.write( updated_vsav_data ) temp_file.write( updated_vsav_data )
temp_file.close() temp_file.close( delete=False )
updated_vsav_dump = _dump_vsav( temp_file.name ) updated_vsav_dump = _dump_vsav( temp_file.name )
expected = { expected = {
"scenario": "Modified scenario name (<>{}\"'\\)", "scenario": "Modified scenario name (<>{}\"'\\)",
@ -862,7 +862,7 @@ def _update_vsav_and_dump( fname, expected ):
# dump the updated VSAV # dump the updated VSAV
with TempFile() as temp_file: with TempFile() as temp_file:
temp_file.write( updated_vsav_data ) temp_file.write( updated_vsav_data )
temp_file.close() temp_file.close( delete=False )
return _dump_vsav( temp_file.name ) return _dump_vsav( temp_file.name )
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

@ -9,6 +9,7 @@ import typing
import uuid import uuid
from collections import defaultdict from collections import defaultdict
import lxml.html
import pytest import pytest
from PyQt5.QtWidgets import QApplication from PyQt5.QtWidgets import QApplication
from selenium.webdriver.support.ui import Select 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.""" """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.""" """Select a droplist option by index."""
options = get_droplist_vals( sel ) 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.""" """Select a droplist option."""
sel_name = sel._el.get_attribute( "name" ) #pylint: disable=protected-access sel_name = sel._el.get_attribute( "name" ) #pylint: disable=protected-access
_webdriver.execute_script( elem = find_child( "select[name='{}']".format( sel_name ) )
"$(arguments[0]).val( '{}' ).trigger( 'change' )".format( val ), _webdriver.execute_script( "$(arguments[0]).val( '{}' )".format( val ), elem )
find_child( "select[name='{}']".format( sel_name ) ) 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 ): def get_droplist_vals_index( sel ):
"""Get the value/text for each option in a droplist.""" """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>" ] _IE_HTML_TAGS = [ "<i>" ]
def adjust_html( val ): def adjust_html( val ):

@ -1,6 +1,7 @@
""" Miscellaneous utilities. """ """ Miscellaneous utilities. """
import os import os
import shutil
import io import io
import tempfile import tempfile
import pathlib import pathlib
@ -62,12 +63,13 @@ class TempFile:
self.temp_file = None self.temp_file = None
self.name = None self.name = None
def __enter__( self ): def open( self ):
"""Allocate a temp file.""" """Allocate a temp file."""
if self.encoding: if self.encoding:
encoding = self.encoding encoding = self.encoding
else: else:
encoding = "utf-8" if "b" not in self.mode else None encoding = "utf-8" if "b" not in self.mode else None
assert self.temp_file is None
self.temp_file = tempfile.NamedTemporaryFile( self.temp_file = tempfile.NamedTemporaryFile(
mode = self.mode, mode = self.mode,
encoding = encoding, encoding = encoding,
@ -75,20 +77,32 @@ class TempFile:
delete = False delete = False
) )
self.name = self.temp_file.name self.name = self.temp_file.name
return self
def __exit__( self, exc_type, exc_val, exc_tb ): def close( self, delete ):
"""Clean up the temp file.""" """Close the temp file."""
self.close() self.temp_file.close()
os.unlink( self.temp_file.name ) if delete:
os.unlink( self.temp_file.name )
def write( self, data ): def write( self, data ):
"""Write data to the temp file.""" """Write data to the temp file."""
self.temp_file.write( data ) self.temp_file.write( data )
def close( self ): def save_copy( self, fname, logger, caption ):
"""Close the temp file.""" """Make a copy of the temp file (for debugging porpoises)."""
self.temp_file.close() 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 # save the VSAV data in a temp file
input_file.write( vsav_data ) input_file.write( vsav_data )
input_file.close() input_file.close( delete=False )
fname = app.config.get( "UPDATE_VSAV_INPUT" ) # nb: for diagnosing problems fname = app.config.get( "UPDATE_VSAV_INPUT" ) # nb: for diagnosing problems
if fname: if fname:
logger.debug( "Saving a copy of the VSAV data: %s", 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: with TempFile() as snippets_file:
# save the snippets in a temp file # save the snippets in a temp file
xml = _save_snippets( snippets, players, snippets_file, logger ) 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 fname = app.config.get( "UPDATE_VSAV_SNIPPETS" ) # nb: for diagnosing problems
if fname: if fname:
logger.debug( "Saving a copy of the snippets: %s", 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 # run the VASSAL shim to update the VSAV file
with TempFile() as output_file, TempFile() as report_file: with TempFile() as output_file, TempFile() as report_file:
output_file.close() output_file.close( delete=False )
report_file.close() report_file.close( delete=False )
vassal_shim = VassalShim() vassal_shim = VassalShim()
vassal_shim.update_scenario( vassal_shim.update_scenario(
input_file.name, snippets_file.name, output_file.name, report_file.name 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 fname = app.config.get( "UPDATE_VSAV_RESULT" ) # nb: for diagnosing problems
if fname: if fname:
logger.debug( "Saving a copy of the updated VSAV: %s", 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 ) fp.write( vsav_data )
# read the report # read the report
report = _parse_label_report( report_file.name ) report = _parse_label_report( report_file.name )
@ -221,7 +221,7 @@ def analyze_vsav():
# save the VSAV data in a temp file # save the VSAV data in a temp file
input_file.write( vsav_data ) input_file.write( vsav_data )
input_file.close() input_file.close( delete=False )
fname = app.config.get( "ANALYZE_VSAV_INPUT" ) # nb: for diagnosing problems fname = app.config.get( "ANALYZE_VSAV_INPUT" ) # nb: for diagnosing problems
if fname: if fname:
logger.debug( "Saving a copy of the VSAV data: %s", 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 # run the VASSAL shim to analyze the VSAV file
with TempFile() as report_file: with TempFile() as report_file:
report_file.close() report_file.close( delete=False )
vassal_shim = VassalShim() vassal_shim = VassalShim()
vassal_shim.analyze_scenario( input_file.name, report_file.name ) vassal_shim.analyze_scenario( input_file.name, report_file.name )
report = _parse_analyze_report( report_file.name ) report = _parse_analyze_report( report_file.name )
@ -307,7 +307,7 @@ class VassalShim:
return None return None
# FUDGE! We can't capture the output on Windows, get the result in a temp file instead :-/ # FUDGE! We can't capture the output on Windows, get the result in a temp file instead :-/
with TempFile() as temp_file: 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 VassalShim()._run_vassal_shim( "version", temp_file.name ) #pylint: disable=protected-access
with open( temp_file.name, "r" ) as fp: with open( temp_file.name, "r" ) as fp:
return fp.read() return fp.read()
@ -319,7 +319,7 @@ class VassalShim:
def analyze_scenario( self, vsav_fname, report_fname ): def analyze_scenario( self, vsav_fname, report_fname ):
"""Analyze a scenario file.""" """Analyze a scenario file."""
return self._run_vassal_shim( 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 ): 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 "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 def _run_vassal_shim( self, *args ): #pylint: disable=too-many-locals
"""Run the VASSAL shim.""" """Run the VASSAL shim."""
@ -352,7 +358,7 @@ class VassalShim:
java_path, "-classpath", class_path, "vassal_shim.Main", java_path, "-classpath", class_path, "vassal_shim.Main",
args[0] args[0]
] ]
if args[0] in ("dump","analyze","update"): if args[0] in ("dump","analyze","analyzeLogs","update"):
if not globvars.vasl_mod: if not globvars.vasl_mod:
raise SimpleError( "The VASL module has not been configured." ) raise SimpleError( "The VASL module has not been configured." )
args2.append( globvars.vasl_mod.filename ) args2.append( globvars.vasl_mod.filename )
@ -394,9 +400,9 @@ class VassalShim:
except subprocess.TimeoutExpired: except subprocess.TimeoutExpired:
proc.kill() proc.kill()
raise raise
buf1.close() buf1.close( delete=False )
stdout = open( buf1.name, "r", encoding="utf-8" ).read() stdout = open( buf1.name, "r", encoding="utf-8" ).read()
buf2.close() buf2.close( delete=False )
stderr = open( buf2.name, "r", encoding="utf-8" ).read() stderr = open( buf2.name, "r", encoding="utf-8" ).read()
elapsed_time = time.time() - start_time elapsed_time = time.time() - start_time
logger.info( "- Completed OK: %.3fs", elapsed_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, # 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 :-/ # but using a temp file is straight-forward and pretty much guaranteed to work :-/
html_tempfile.write( html ) html_tempfile.write( html )
html_tempfile.close() html_tempfile.close( delete=False )
self.driver.get( "file://{}".format( html_tempfile.name ) ) self.driver.get( "file://{}".format( html_tempfile.name ) )
# take a screenshot of the HTML # 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] ) self.driver.set_window_size( window_size[0], window_size[1] )
img = do_get_screenshot( screenshot_tempfile.name ) img = do_get_screenshot( screenshot_tempfile.name )
retry_ratio = float( app.config.get( "WEBDRIVER_SCREENSHOT_RETRY_RATIO", 0.8 ) ) 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.BufferedWriter ;
import java.io.FileWriter ; import java.io.FileWriter ;
import java.util.ArrayList ;
import VASSAL.Info ; import VASSAL.Info ;
@ -23,25 +24,34 @@ public class Main
try { try {
String cmd = args[0].toLowerCase() ; String cmd = args[0].toLowerCase() ;
if ( cmd.equals( "dump" ) ) { 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 ) ; VassalShim shim = new VassalShim( args[1], null ) ;
shim.dumpScenario( args[2] ) ; shim.dumpScenario( args[2] ) ;
System.exit( 0 ) ; System.exit( 0 ) ;
} }
else if ( cmd.equals( "analyze" ) ) { else if ( cmd.equals( "analyze" ) ) {
checkArgs( args, 5, "the VASL .vmod file, boards directory, scenario file and output file" ) ; checkArgs( args, 4, false, "the VASL .vmod file, scenario file and output file" ) ;
VassalShim shim = new VassalShim( args[1], args[2] ) ; VassalShim shim = new VassalShim( args[1], null ) ;
shim.analyzeScenario( args[3], args[4] ) ; 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 ) ; System.exit( 0 ) ;
} }
else if ( cmd.equals( "update" ) ) { 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] ) ; VassalShim shim = new VassalShim( args[1], args[2] ) ;
shim.updateScenario( args[3], args[4], args[5], args[6] ) ; shim.updateScenario( args[3], args[4], args[5], args[6] ) ;
System.exit( 0 ) ; System.exit( 0 ) ;
} }
else if ( cmd.equals( "version" ) ) { else if ( cmd.equals( "version" ) ) {
checkArgs( args, 2, "the output file" ) ; checkArgs( args, 2, false, "the output file" ) ;
System.out.println( Info.getVersion() ) ; System.out.println( Info.getVersion() ) ;
// FUDGE! The Python web server can't capture output on Windows - save the result to a file as well :-/ // 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] ) ) ; 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 // 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.out.println( "Incorrect number of arguments, please specify " + hint + "." ) ;
System.exit( 2 ) ; System.exit( 2 ) ;
} }

@ -9,6 +9,7 @@ import javax.xml.transform.TransformerException ;
import javax.xml.transform.TransformerConfigurationException ; import javax.xml.transform.TransformerConfigurationException ;
import javax.xml.transform.dom.DOMSource ; import javax.xml.transform.dom.DOMSource ;
import javax.xml.transform.stream.StreamResult ; import javax.xml.transform.stream.StreamResult ;
import java.util.regex.Matcher ;
import org.w3c.dom.Document ; import org.w3c.dom.Document ;
import org.w3c.dom.NodeList ; import org.w3c.dom.NodeList ;
@ -44,6 +45,16 @@ public class Utils
return buf.toString() ; 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 ) public static boolean startsWith( String val, String target )
{ {
// check if a string starts with a target substring // check if a string starts with a target substring

@ -44,9 +44,11 @@ import VASSAL.build.GameModule ;
import VASSAL.build.GpIdChecker ; import VASSAL.build.GpIdChecker ;
import VASSAL.build.module.GameState ; import VASSAL.build.module.GameState ;
import VASSAL.build.module.GameComponent ; import VASSAL.build.module.GameComponent ;
import VASSAL.build.module.BasicLogger.LogCommand ;
import VASSAL.build.module.ModuleExtension ; import VASSAL.build.module.ModuleExtension ;
import VASSAL.build.module.ObscurableOptions ; import VASSAL.build.module.ObscurableOptions ;
import VASSAL.build.module.metadata.SaveMetaData ; import VASSAL.build.module.metadata.SaveMetaData ;
import VASSAL.build.module.Chatter.DisplayText ;
import VASSAL.build.widget.PieceSlot ; import VASSAL.build.widget.PieceSlot ;
import VASSAL.launch.BasicModule ; import VASSAL.launch.BasicModule ;
import VASSAL.command.Command ; import VASSAL.command.Command ;
@ -70,14 +72,7 @@ import VASSAL.tools.io.ObfuscatingOutputStream ;
import VASSAL.tools.io.ZipArchive ; import VASSAL.tools.io.ZipArchive ;
import VASSAL.i18n.Resources ; import VASSAL.i18n.Resources ;
import vassal_shim.Snippet ; import vassal_shim.lfa.* ;
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 ;
// -------------------------------------------------------------------- // --------------------------------------------------------------------
@ -144,11 +139,144 @@ public class VassalShim
dumpCommand( cmd, "" ) ; 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 ) public void analyzeScenario( String scenarioFilename, String reportFilename )
throws IOException, ParserConfigurationException, TransformerConfigurationException, TransformerException throws IOException, ParserConfigurationException, TransformerConfigurationException, TransformerException
{ {
// load the scenario // load the scenario
configureBoards() ;
Command cmd = loadScenario( scenarioFilename ) ; Command cmd = loadScenario( scenarioFilename ) ;
cmd.execute() ; cmd.execute() ;
@ -219,7 +347,7 @@ public class VassalShim
ArrayList<GamePieceLabelFields> otherLabels = new ArrayList<GamePieceLabelFields>() ; ArrayList<GamePieceLabelFields> otherLabels = new ArrayList<GamePieceLabelFields>() ;
logger.info( "Searching the VASL scenario for labels (players={};{})...", players[0], players[1] ) ; logger.info( "Searching the VASL scenario for labels (players={};{})...", players[0], players[1] ) ;
AppBoolean hasPlayerOwnedLabels = new AppBoolean( false ) ; 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". // 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. // 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 // check if this command is a label we're interested in
// NOTE: We shouldn't really be looking at the object type, see analyzeScenario(). // 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" ) ) { if ( gamePiece.getName().equals( "User-Labeled" ) ) {
// yup - parse the label content // 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> separators = new ArrayList<String>() ;
ArrayList<String> fields = 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 // check if the label is one of ours
String snippetId = isVaslTemplatesLabel( fields, GamePieceLabelFields.FIELD_INDEX_LABEL1 ) ; String snippetId = isVaslTemplatesLabel( fields, GamePieceLabelFields.FIELD_INDEX_LABEL1 ) ;
@ -370,7 +506,7 @@ public class VassalShim
// extract labels in sub-commands // extract labels in sub-commands
for ( Command c: cmd.getSubCommands() ) 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 ) private String isVaslTemplatesLabel( ArrayList<String> fields, int fieldIndex )
@ -894,6 +1030,8 @@ public class VassalShim
dumpCommandExtras( (GameState.SetupCommand)cmd, buf, prefix ) ; dumpCommandExtras( (GameState.SetupCommand)cmd, buf, prefix ) ;
else if ( cmd instanceof ModuleExtension.RegCmd ) else if ( cmd instanceof ModuleExtension.RegCmd )
dumpCommandExtras( (ModuleExtension.RegCmd)cmd, buf, prefix ) ; dumpCommandExtras( (ModuleExtension.RegCmd)cmd, buf, prefix ) ;
else if ( cmd instanceof LogCommand )
dumpCommandExtras( (LogCommand)cmd, buf, prefix ) ;
else if ( cmd instanceof ObscurableOptions.SetAllowed ) else if ( cmd instanceof ObscurableOptions.SetAllowed )
dumpCommandExtras( (ObscurableOptions.SetAllowed)cmd, buf, prefix ) ; dumpCommandExtras( (ObscurableOptions.SetAllowed)cmd, buf, prefix ) ;
System.out.println( buf.toString() ) ; System.out.println( buf.toString() ) ;
@ -945,6 +1083,12 @@ public class VassalShim
buf.append( ": " + cmd.getName() + " (" + cmd.getVersion() + ")" ) ; 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 ) private static void dumpCommandExtras( ObscurableOptions.SetAllowed cmd, StringBuilder buf, String prefix )
{ {
// dump extra command info // 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