Compare commits

...

111 Commits
v1.9 ... master

Author SHA1 Message Date
Pacman Ghost 85616567f8 Updated the help. 3 weeks ago
Pacman Ghost f20693f8ae Added support for VASL 6.6.8 and VASSAL 3.7.9. 3 weeks ago
Pacman Ghost f87acbea56 Handle SIGTERM and shutdown gracefully. 3 weeks ago
Pacman Ghost 19002a03e5 Updated the version strings. 3 weeks ago
Pacman Ghost 396ebcef73 Allow ASL_RULEBOOK2_BASE_URL to be set from an env.var. 1 month ago
Pacman Ghost 24106fb7e5 Updated the documentation. 5 months ago
Pacman Ghost 2b4bc9045a Updated for VASL 6.6.7 and VASSAL 3.7.5. 5 months ago
Pacman Ghost 71b07accd8 Disable the context help button in the title bar (Windows). 6 months ago
Pacman Ghost acdb8351ef Added a link to the repo issues page in the About box. 6 months ago
Pacman Ghost 5606fe0d1a Show a better error message if Java is not configured. 6 months ago
Pacman Ghost cb0369b85a Show the "Program Info" menu item in the desktop program. 6 months ago
Pacman Ghost fdaf2f0cde Added logging for testing different VASSAL+VASL combinations. 6 months ago
Pacman Ghost bb075ef0da Tidied up some test fixtures. 6 months ago
Pacman Ghost a30cbbb616 Applied errata from ASLJ 13 for when the KMC get black OBA numbers. 6 months ago
Pacman Ghost 3e7daad138 Use blue counters for some French ordnance. 6 months ago
Pacman Ghost 2c6f0f2ba3 Updated the dependencies. 6 months ago
Pacman Ghost 127c5d46e0 Updated the dependencies. 7 months ago
Pacman Ghost 4df0f5cf77 Updated the version number to v1.12. 1 year ago
Pacman Ghost 8269917bee Updated the documentation 1 year ago
Pacman Ghost a274775bf7 Updated for VASSAL 3.6.14 and VASL 6.6.6. 1 year ago
Pacman Ghost 1c28ba4373 Show a better error message if the VASL data directory is not there. 1 year ago
Pacman Ghost bc87c398bb Added options to configure logging when running Docker. 1 year ago
Pacman Ghost 1e72a57d8d Added options to store data files outside a Docker container. 1 year ago
Pacman Ghost 8b8c3b2698 Fixed how external links in the help are opened. 1 year ago
Pacman Ghost 51a5bcee90 Booby Trap labels are now associated with a player nationality. 1 year ago
Pacman Ghost 6f070eb17a Correctly flag pieces from extensions as small. 1 year ago
Pacman Ghost 21d7aa2b68 Tweaked the display format for the Docker build timestamp. 1 year ago
Pacman Ghost 632e0e7e6d Tweaked a Trumbowyg icon. 1 year ago
Pacman Ghost 2c02163c46 Don't automatically create THH labels. 1 year ago
Pacman Ghost 20739d78de Updated the version strings. 1 year ago
Pacman Ghost 19734ac076 Updated for VASSAL 3.6.7 and VASL 6.6.5. 2 years ago
Pacman Ghost 85b625d54e Updated links to point to pacman-ghost.com. 2 years ago
Pacman Ghost 446a53b32f Fixed a possible timing error during startup. 2 years ago
Pacman Ghost c7a002b1c6 Fixed a test to work inside a Docker container with no internet access. 2 years ago
Pacman Ghost 8172fa7070 Allow the Canadians to generate a PIAT snippet. 2 years ago
Pacman Ghost 330d05e47d Don't show the loading spinner if Javascript is disabled. 2 years ago
Pacman Ghost b285a044ab Updated the documentation. 2 years ago
Pacman Ghost c555614ac6 Updated the examples. 2 years ago
Pacman Ghost 618fe3bd68 Tweaked a log message. 2 years ago
Pacman Ghost 0b720dc8bc Automatically set up basic logging for the desktop app. 2 years ago
Pacman Ghost 499e5645bc When loading a scenario, forget the previous .vsav file used. 2 years ago
Pacman Ghost fc3e1110f8 Worked around a layout problem when the Victory Conditions are long. 2 years ago
Pacman Ghost 8388cec67e Added large flags to the WYSIWYG editor's dropdown. 2 years ago
Pacman Ghost 569edbc48a Added shortcuts for Alt-Shift-< and >. 2 years ago
Pacman Ghost db7fecbc39 Optimized searching for scenarios. 2 years ago
Pacman Ghost 380dae5559 Merged the Extras templates for Kampfgruppe Scherer. 2 years ago
Pacman Ghost e5ad66658a Set focus to the correct default button if there are nested dialogs. 2 years ago
Pacman Ghost c5e1e53df4 Extended the files/directories monitored for Flask hot-reloading. 2 years ago
Pacman Ghost d00d809877 Don't reset the current directory for saving/loading files on "new scenario". 2 years ago
Pacman Ghost c4c2010170 Updated the National Capabilities label for the Free French. 2 years ago
Pacman Ghost c2e0b48343 Added Alt-Click handlers. 2 years ago
Pacman Ghost 0e0df4e3a6 Tightened up some tests. 2 years ago
Pacman Ghost d78ac34c1f Handle missing landing craft Chapter H notes. 2 years ago
Pacman Ghost 05933ad753 Reset the HTML editor controls each time they are used. 2 years ago
Pacman Ghost f6e5081605 Adjusted the layout of the SCENARIO panel. 2 years ago
Pacman Ghost b31f64ed7f Show an indicator when an HTML textbox goes multi-line. 2 years ago
Pacman Ghost 2d317c57bd Moved the "edit template" buttons into a drop-down menu. 2 years ago
Pacman Ghost 941439a2ae Allowed Extras templates to set the width of their dropdown's. 2 years ago
Pacman Ghost 071b6f587d Stopped incorrectly detecting "split MG" v/o comments as having been changed. 2 years ago
Pacman Ghost 437865b47d Improved time-based comments in the Axis Minor nat.caps. snippets. 2 years ago
Pacman Ghost 650a6f86d4 Grouped notes in Nationality Capabilities snippets. 2 years ago
Pacman Ghost 0682dc1dca Added an Extras template for Booby Traps. 2 years ago
Pacman Ghost 4ae19ca212 Tightened up how we detect if HTML content has been changed. 2 years ago
Pacman Ghost e7888e88e0 Cached V/O note images for extensions are now saved in their own sub-directory. 2 years ago
Pacman Ghost 1294d0e3d2 Store config and data files in the standard locations. 2 years ago
Pacman Ghost fbcf4e9184 Confirm closing the EDIT V/O dialog if changes have been made. 2 years ago
Pacman Ghost 6d261032be Improved the keyboard interface in the TURN TRACK dialog. 2 years ago
Pacman Ghost 4629b3421b Only show custom list bullets in the UI if they have been enabled. 2 years ago
Pacman Ghost 53e14f753f Allow single-line textbox's to be edited as HTML. 2 years ago
Pacman Ghost a09286bc50 Include the build git info in the SCENARIO template. 2 years ago
Pacman Ghost 4762a36eb6 Removed code supporting Internet Explorer. 2 years ago
Pacman Ghost 23256bbc63 Tightened up some tests. 2 years ago
Pacman Ghost 2628105ea9 Minor UI tweaks. 2 years ago
Pacman Ghost 77b1bc9548 Allow SSR snippets to be auto-generated. 2 years ago
Pacman Ghost ee4d8fbe02 Fixed a problem when auto-generating a simple note snippet with no content. 2 years ago
Pacman Ghost 2ee42f37b5 Tightened up the presentation of lists in the UI and snippets. 2 years ago
Pacman Ghost 15c632b522 Sanitize HTML content. 2 years ago
Pacman Ghost c420c168d1 Added a WYSIWYG editor for HTML content. 2 years ago
Pacman Ghost d5021f7960 Added the ASA and ROAR ID's to the SCENARIO template. 2 years ago
Pacman Ghost daef7c1c15 Gave VASSAL tests more time to run. 2 years ago
Pacman Ghost b10105f2a7 Improved the positioning and sizing of dialogs. 2 years ago
Pacman Ghost 6b909a37eb Changed how flags are resized. 2 years ago
Pacman Ghost 7e070c3b68 Update the UI after importing an ASA scenario. 2 years ago
Pacman Ghost 0312fa2fc7 Confirm closing the "add/edit simple note" dialog if changes have been made. 2 years ago
Pacman Ghost 95167f4888 Allow snippets to be generated from the "add/edit simple note" dialog. 2 years ago
Pacman Ghost 2a707863d2 Fixed a timing error during startup. 2 years ago
Pacman Ghost 13a1b511e6 Added splitters to make some UI elements resizable. 2 years ago
Pacman Ghost a556d22117 Got 1/2" counters showing at the smaller size again. 2 years ago
Pacman Ghost a52e7f8ba9 Added a splitter to the "edit vehicle/ordnance" dialog. 2 years ago
Pacman Ghost fa5a99e87c Changed how Java is detected (and reported) from the PATH. 2 years ago
Pacman Ghost 7f17a634eb Updated some icons, tightened up the UI. 2 years ago
Pacman Ghost fbc420d4b3 Tightened up the player flags. 2 years ago
Pacman Ghost feebe9f63c Disable snippet-width textbox's when disabling snippet buttons. 2 years ago
Pacman Ghost e479bf2a76 Removed the "Width:" labels from the UI. 2 years ago
Pacman Ghost 520ea43d9d Tidy-up strings coming from ROAR and ASA. 2 years ago
Pacman Ghost ff0f40b22b Tightened up how we map ASA theaters to our own. 2 years ago
Pacman Ghost 55c15e5157 Made the compass label part of the default template pack. 2 years ago
Pacman Ghost 757c78dbd8 Removed the text caption from some snippet buttons. 2 years ago
Pacman Ghost 1079ac7e60 Got the LFA tests going again in Firefox. 2 years ago
Pacman Ghost d165bfdefd Allow multiple levels of shading in the turn track. 2 years ago
Pacman Ghost 729c6e306d Reset the turn track when importing a scenario. 2 years ago
Pacman Ghost cd831446fd Allow turn track squares to be shaded. 2 years ago
Pacman Ghost 40c2b87cbe Added a new extras template for the compass. 2 years ago
Pacman Ghost bc724e426d Removed the option to create National Capabilities labels. 2 years ago
Pacman Ghost c850bbd281 Tweaked the presentation of the "VASSAL shim error" dialog. 2 years ago
Pacman Ghost 42cad6f87c Fixed a resizing problem in the "connect to ROAR" dialog. 2 years ago
Pacman Ghost c363acb73e Added more padding to dialogs. 2 years ago
Pacman Ghost c5cdaf27dd Return a spacer image as a flag for nationalities that don't have one. 2 years ago
Pacman Ghost 0966c08dd4 Added the turn track template. 2 years ago
Pacman Ghost 3a5e472b36 Updated dependencies. 2 years ago
Pacman Ghost f93888c51b Updated the version strings. 2 years ago
  1. 89
      .pylintrc
  2. 14
      Dockerfile
  3. 9
      README.md
  4. 32
      conftest.py
  5. 1
      docker/config/site.cfg
  6. 6
      docker/run.sh
  7. BIN
      examples/Hill 621 (Scenario E) (online).vsav
  8. 32
      examples/Hill 621 (Scenario E).json
  9. BIN
      examples/Hill 621 (Scenario E).png
  10. BIN
      examples/Hill 621 (Scenario E).small.jpg
  11. BIN
      examples/Hill 621 (Scenario E).vsav
  12. BIN
      examples/Hube's Pocket (Scenario G) (online).vsav
  13. 30
      examples/Hube's Pocket (Scenario G).json
  14. BIN
      examples/Hube's Pocket (Scenario G).png
  15. BIN
      examples/Hube's Pocket (Scenario G).small.jpg
  16. BIN
      examples/Hube's Pocket (Scenario G).vsav
  17. 2
      examples/README.md
  18. BIN
      examples/The Streets Of Stalingrad (Scenario C) (online).vsav
  19. 30
      examples/The Streets Of Stalingrad (Scenario C).json
  20. BIN
      examples/The Streets Of Stalingrad (Scenario C).png
  21. BIN
      examples/The Streets Of Stalingrad (Scenario C).small.jpg
  22. BIN
      examples/The Streets Of Stalingrad (Scenario C).vsav
  23. 4
      freeze.py
  24. 2
      loader/freeze.py
  25. 6
      loader/main.py
  26. 14
      requirements-dev.txt
  27. 16
      requirements.txt
  28. 97
      run-container.sh
  29. 4
      setup.py
  30. 9
      vasl_templates/about.py
  31. 19
      vasl_templates/file_dialog.py
  32. 28
      vasl_templates/main.py
  33. 21
      vasl_templates/main_window.py
  34. 4
      vasl_templates/server_settings.py
  35. 83
      vasl_templates/tools/get_piece_info.py
  36. 49
      vasl_templates/ui/about.ui
  37. 2
      vasl_templates/ui/server_settings.ui
  38. 26
      vasl_templates/utils.py
  39. 4
      vasl_templates/web_channel.py
  40. 70
      vasl_templates/webapp/__init__.py
  41. 3
      vasl_templates/webapp/config/constants.py
  42. 4
      vasl_templates/webapp/config/logging.yaml.example
  43. 2
      vasl_templates/webapp/config/site.cfg.example
  44. 5
      vasl_templates/webapp/data/asl-scenario-archive.json
  45. 2
      vasl_templates/webapp/data/default-template-pack/common.css
  46. 5
      vasl_templates/webapp/data/default-template-pack/compass.j2
  47. 41
      vasl_templates/webapp/data/default-template-pack/extras/booby-traps.j2
  48. 2
      vasl_templates/webapp/data/default-template-pack/extras/count-remaining.j2
  49. 4
      vasl_templates/webapp/data/default-template-pack/extras/grid.j2
  50. 2
      vasl_templates/webapp/data/default-template-pack/extras/kakazu-ridge-cave-complexes.j2
  51. 36
      vasl_templates/webapp/data/default-template-pack/extras/kgs.j2
  52. 49
      vasl_templates/webapp/data/default-template-pack/extras/kgs/grenade-bundles.j2
  53. 2
      vasl_templates/webapp/data/default-template-pack/extras/victory-points.j2
  54. 15
      vasl_templates/webapp/data/default-template-pack/nat_caps.j2
  55. 220
      vasl_templates/webapp/data/default-template-pack/national-capabilities.json
  56. 5
      vasl_templates/webapp/data/default-template-pack/ob_vo_note.css
  57. 2
      vasl_templates/webapp/data/default-template-pack/player_flag.include
  58. 2
      vasl_templates/webapp/data/default-template-pack/player_flag_large.include
  59. 4
      vasl_templates/webapp/data/default-template-pack/players.j2
  60. 4
      vasl_templates/webapp/data/default-template-pack/scenario.j2
  61. 2
      vasl_templates/webapp/data/default-template-pack/ssr.j2
  62. 116
      vasl_templates/webapp/data/default-template-pack/turn_track.j2
  63. 6
      vasl_templates/webapp/data/ordnance/french.json
  64. 65
      vasl_templates/webapp/data/vasl-6.6.0/piece-info.json
  65. 65
      vasl_templates/webapp/data/vasl-6.6.1/piece-info.json
  66. 65
      vasl_templates/webapp/data/vasl-6.6.2/piece-info.json
  67. 65
      vasl_templates/webapp/data/vasl-6.6.3/piece-info.json
  68. 65
      vasl_templates/webapp/data/vasl-6.6.4/piece-info.json
  69. 49
      vasl_templates/webapp/data/vasl-6.6.5/expected-multiple-images.json
  70. 2
      vasl_templates/webapp/data/vasl-6.6.5/online-counter-images.json
  71. 65
      vasl_templates/webapp/data/vasl-6.6.5/piece-info.json
  72. 147
      vasl_templates/webapp/data/vasl-6.6.5/vasl-overrides.json
  73. 61
      vasl_templates/webapp/data/vasl-6.6.6/expected-multiple-images.json
  74. 2
      vasl_templates/webapp/data/vasl-6.6.6/online-counter-images.json
  75. 71
      vasl_templates/webapp/data/vasl-6.6.6/piece-info.json
  76. 147
      vasl_templates/webapp/data/vasl-6.6.6/vasl-overrides.json
  77. 72
      vasl_templates/webapp/data/vasl-6.6.7/expected-multiple-images.json
  78. 2
      vasl_templates/webapp/data/vasl-6.6.7/online-counter-images.json
  79. 71
      vasl_templates/webapp/data/vasl-6.6.7/piece-info.json
  80. 147
      vasl_templates/webapp/data/vasl-6.6.7/vasl-overrides.json
  81. 72
      vasl_templates/webapp/data/vasl-6.6.8/expected-multiple-images.json
  82. 2
      vasl_templates/webapp/data/vasl-6.6.8/online-counter-images.json
  83. 71
      vasl_templates/webapp/data/vasl-6.6.8/piece-info.json
  84. 147
      vasl_templates/webapp/data/vasl-6.6.8/vasl-overrides.json
  85. 8
      vasl_templates/webapp/data/vehicles/german.json
  86. 23
      vasl_templates/webapp/downloads.py
  87. 1
      vasl_templates/webapp/globvars.py
  88. 2
      vasl_templates/webapp/lfa.py
  89. 87
      vasl_templates/webapp/main.py
  90. 3
      vasl_templates/webapp/run_server.py
  91. 34
      vasl_templates/webapp/scenarios.py
  92. 64
      vasl_templates/webapp/snippets.py
  93. 1679
      vasl_templates/webapp/static/DOMPurify/purify.js
  94. 1
      vasl_templates/webapp/static/DOMPurify/purify.js.map
  95. 3
      vasl_templates/webapp/static/DOMPurify/purify.min.js
  96. 1
      vasl_templates/webapp/static/DOMPurify/purify.min.js.map
  97. 5
      vasl_templates/webapp/static/css/custom-bullets.css
  98. 11
      vasl_templates/webapp/static/css/desktop.css
  99. 4
      vasl_templates/webapp/static/css/edit-html-textbox-dialog.css
  100. 4
      vasl_templates/webapp/static/css/edit-simple-note-dialog.css
  101. Some files were not shown because too many files have changed in this diff Show More

@ -54,88 +54,14 @@ confidence=
# --enable=similarities". If you want to run only the classes checker, but have # --enable=similarities". If you want to run only the classes checker, but have
# no Warning level messages displayed, use"--disable=all --enable=classes # no Warning level messages displayed, use"--disable=all --enable=classes
# --disable=W" # --disable=W"
disable=print-statement, disable=
parameter-unpacking,
unpacking-in-except,
old-raise-syntax,
backtick,
long-suffix,
old-ne-operator,
old-octal-literal,
import-star-module-level,
non-ascii-bytes-literal,
invalid-unicode-literal,
raw-checker-failed, raw-checker-failed,
bad-inline-option, bad-inline-option,
locally-disabled, locally-disabled,
locally-enabled,
file-ignored, file-ignored,
suppressed-message, suppressed-message,
useless-suppression, useless-suppression,
deprecated-pragma, deprecated-pragma,
apply-builtin,
basestring-builtin,
buffer-builtin,
cmp-builtin,
coerce-builtin,
execfile-builtin,
file-builtin,
long-builtin,
raw_input-builtin,
reduce-builtin,
standarderror-builtin,
unicode-builtin,
xrange-builtin,
coerce-method,
delslice-method,
getslice-method,
setslice-method,
no-absolute-import,
old-division,
dict-iter-method,
dict-view-method,
next-method-called,
metaclass-assignment,
indexing-exception,
raising-string,
reload-builtin,
oct-method,
hex-method,
nonzero-method,
cmp-method,
input-builtin,
round-builtin,
intern-builtin,
unichr-builtin,
map-builtin-not-iterating,
zip-builtin-not-iterating,
range-builtin-not-iterating,
filter-builtin-not-iterating,
using-cmp-argument,
eq-without-hash,
div-method,
idiv-method,
rdiv-method,
exception-message-attribute,
invalid-str-codec,
sys-max-int,
bad-python3-import,
deprecated-string-function,
deprecated-str-translate-call,
deprecated-itertools-function,
deprecated-types-field,
next-method-defined,
dict-items-not-iterating,
dict-keys-not-iterating,
dict-values-not-iterating,
deprecated-operator-function,
deprecated-urllib-function,
xreadlines-attribute,
deprecated-sys-function,
exception-escape,
comprehension-escape,
bad-whitespace,
bad-continuation,
invalid-name, invalid-name,
wrong-import-position, wrong-import-position,
global-statement, global-statement,
@ -149,7 +75,9 @@ disable=print-statement,
isinstance-second-argument-not-valid-type, isinstance-second-argument-not-valid-type,
consider-using-f-string, consider-using-f-string,
consider-using-max-builtin, consider-using-max-builtin,
use-implicit-booleaness-not-comparison use-implicit-booleaness-not-comparison,
unnecessary-lambda-assignment,
duplicate-string-formatting-argument,
# Enable the message, report, category or checker with the given id(s). You can # Enable the message, report, category or checker with the given id(s). You can
# either give multiple identifier separated by comma (,) or put this option # either give multiple identifier separated by comma (,) or put this option
@ -294,13 +222,6 @@ max-line-length=120
# Maximum number of lines in a module # Maximum number of lines in a module
max-module-lines=1000 max-module-lines=1000
# List of optional constructs for which whitespace checking is disabled. `dict-
# separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}.
# `trailing-comma` allows a space between comma and closing bracket: (a, ).
# `empty-line` allows space-only lines.
no-space-check=trailing-comma,
dict-separator
# Allow the body of a class to be on the same line as the declaration if body # Allow the body of a class to be on the same line as the declaration if body
# contains single statement. # contains single statement.
single-line-class-stmt=no single-line-class-stmt=no
@ -559,4 +480,4 @@ known-third-party=enchant
# Exceptions that will emit a warning when being caught. Defaults to # Exceptions that will emit a warning when being caught. Defaults to
# "Exception" # "Exception"
overgeneral-exceptions=Exception overgeneral-exceptions=builtins.Exception

@ -1,28 +1,28 @@
# NOTE: Use the run-container.sh script to build and launch this container. # NOTE: Use the run-container.sh script to build and launch this container.
# NOTE: Multi-stage builds require Docker >= 17.05. # NOTE: Multi-stage builds require Docker >= 17.05.
FROM rockylinux:8 AS base FROM rockylinux:9.1 AS base
# update packages and install requirements # update packages and install requirements
RUN dnf -y upgrade-minimal && \ RUN dnf -y upgrade-minimal && \
dnf install -y python38 dnf install -y python3.11
# NOTE: We don't need the following stuff for the build step, but it's nice to not have to re-install # NOTE: We don't need the following stuff for the build step, but it's nice to not have to re-install
# it all every time we change the requirements :-/ # it all every time we change the requirements :-/
# install Java # install Java
ARG JAVA_URL=https://download.java.net/java/GA/jdk15.0.1/51f4f36ad4ef43e39d0dfdbaf6549e32/9/GPL/openjdk-15.0.1_linux-x64_bin.tar.gz RUN dnf install -y java-17-openjdk
RUN curl -s "$JAVA_URL" | tar -xz -C /usr/bin/
# install Firefox # install Firefox
ARG FIREFOX_URL=https://ftp.mozilla.org/pub/firefox/releases/94.0.2/linux-x86_64/en-US/firefox-94.0.2.tar.bz2 # NOTE: We could install this using dnf, but the version of geckodriver needs to match it.
ARG FIREFOX_URL=https://ftp.mozilla.org/pub/firefox/releases/117.0.1/linux-x86_64/en-US/firefox-117.0.1.tar.bz2
RUN dnf install -y bzip2 xorg-x11-server-Xvfb gtk3 dbus-glib && \ RUN dnf install -y bzip2 xorg-x11-server-Xvfb gtk3 dbus-glib && \
curl -s "$FIREFOX_URL" | tar -jx -C /usr/local/ && \ curl -s "$FIREFOX_URL" | tar -jx -C /usr/local/ && \
ln -s /usr/local/firefox/firefox /usr/bin/firefox && \ ln -s /usr/local/firefox/firefox /usr/bin/firefox && \
echo "exclude=firefox" >>/etc/dnf/dnf.conf echo "exclude=firefox" >>/etc/dnf/dnf.conf
# install geckodriver # install geckodriver
ARG GECKODRIVER_URL=https://github.com/mozilla/geckodriver/releases/download/v0.30.0/geckodriver-v0.30.0-linux64.tar.gz ARG GECKODRIVER_URL=https://github.com/mozilla/geckodriver/releases/download/v0.33.0/geckodriver-v0.33.0-linux64.tar.gz
RUN curl -sL "$GECKODRIVER_URL" | tar -xz -C /usr/bin/ RUN curl -sL "$GECKODRIVER_URL" | tar -xz -C /usr/bin/
# clean up # clean up
@ -33,7 +33,7 @@ RUN dnf clean all
FROM base AS build FROM base AS build
# set up a virtualenv # set up a virtualenv
RUN python3 -m venv /opt/venv RUN python3.11 -m venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH" ENV PATH="/opt/venv/bin:$PATH"
RUN pip install --upgrade pip RUN pip install --upgrade pip

@ -1,16 +1,14 @@
# VASL Templates # VASL Templates
<a href="https://github.com/pacman-ghost/vasl-templates/raw/master/vasl_templates/webapp/static/help/images/hill-621.png" target="_blank"> [<img src="vasl_templates/webapp/static/help/images/hill-621.small.png" width="200" align="right" hspace="10">](vasl_templates/webapp/static/help/images/hill-621.png)
<img src="https://github.com/pacman-ghost/vasl-templates/raw/master/vasl_templates/webapp/static/help/images/hill-621.small.png" width="200" align="right" hspace="10">
</a>
*VASL Templates* makes it easy to set up attractive VASL scenarios, with loads of useful information embedded to assist with game play. *VASL Templates* makes it easy to set up attractive VASL scenarios, with loads of useful information embedded to assist with game play.
Simply enter the scenario information into the UI, and the program will generate HTML snippets that you can transfer into VASL labels in your scenario. Simply enter the scenario information into the UI, and the program will generate HTML snippets that you can transfer into VASL labels in your scenario.
<img src="https://github.com/pacman-ghost/vasl-templates/raw/master/vasl_templates/webapp/static/help/images/ob_setup.png" width="200"> [<img src="vasl_templates/webapp/static/help/images/ob_setup.png" width="200">](vasl_templates/webapp/static/help/images/ob_setup.png)
You can find more examples of the program in action [here](https://github.com/pacman-ghost/vasl-templates/tree/master/examples/). You can find more examples of the program in action [here](examples/).
### Documentation ### Documentation
@ -20,3 +18,4 @@ You can find more examples of the program in action [here](https://github.com/pa
* [Setting up Chapter H data](https://vasl-templates.org/help?tab=chapterh) * [Setting up Chapter H data](https://vasl-templates.org/help?tab=chapterh)
* [Writing your own templates](https://vasl-templates.org/help?tab=templatepacks) * [Writing your own templates](https://vasl-templates.org/help?tab=templatepacks)
* [For developers](https://vasl-templates.org/help?tab=fordevelopers) * [For developers](https://vasl-templates.org/help?tab=fordevelopers)
* [FAQ](https://vasl-templates.org/FAQ)

@ -1,6 +1,7 @@
""" pytest support functions. """ """ pytest support functions. """
import os import os
import shutil
import threading import threading
import json import json
import re import re
@ -10,8 +11,6 @@ import urllib.request
from urllib.error import URLError from urllib.error import URLError
import pytest import pytest
from flask import url_for
from vasl_templates.webapp import app from vasl_templates.webapp import app
from vasl_templates.webapp.tests import utils from vasl_templates.webapp.tests import utils
from vasl_templates.webapp.tests.control_tests import ControlTests from vasl_templates.webapp.tests.control_tests import ControlTests
@ -19,6 +18,7 @@ from vasl_templates.webapp.tests.control_tests import ControlTests
FLASK_WEBAPP_PORT = 5011 FLASK_WEBAPP_PORT = 5011
_pytest_options = None _pytest_options = None
_orig_url_for = app.url_for
# --------------------------------------------------------------------- # ---------------------------------------------------------------------
@ -42,7 +42,7 @@ def pytest_addoption( parser ):
help="Run the tests headless." help="Run the tests headless."
) )
parser.addoption( parser.addoption(
"--window-size", action="store", dest="window_size", default="1000x700", "--window-size", action="store", dest="window_size", default="1020x700",
help="Browser window size." help="Browser window size."
) )
@ -70,6 +70,13 @@ def pytest_configure( config ):
import vasl_templates.webapp.tests import vasl_templates.webapp.tests
vasl_templates.webapp.tests.pytest_options = config.option vasl_templates.webapp.tests.pytest_options = config.option
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
@pytest.fixture( scope="session" )
def monkeypatch():
"""Override the default monkeypatch fixture."""
assert False, "Don't use monkeypatch!" # it won't work when testing against a remote server
# --------------------------------------------------------------------- # ---------------------------------------------------------------------
_webapp = None _webapp = None
@ -122,7 +129,9 @@ def _make_webapp():
# NOTE: It's not a bad idea to bypass the clipboard, even when running in a browser, # NOTE: It's not a bad idea to bypass the clipboard, even when running in a browser,
# to avoid problems if something else uses the clipboard while the tests are running. # to avoid problems if something else uses the clipboard while the tests are running.
kwargs["store_clipboard"] = 1 kwargs["store_clipboard"] = 1
url = url_for( endpoint, _external=True, **kwargs ) if kwargs.get( "_external" ) is None:
kwargs["_external"] = True
url = _orig_url_for( endpoint, **kwargs )
url = url.replace( "http://localhost", app.base_url ) url = url.replace( "http://localhost", app.base_url )
return url return url
app.url_for = make_webapp_url app.url_for = make_webapp_url
@ -180,9 +189,9 @@ def _make_webapp():
# we won't have even got this far, since it needs to be there to drive the browser. # we won't have even got this far, since it needs to be there to drive the browser.
# NOTE: This will have no effect if we're talking to a remote server, but we can live with that. # NOTE: This will have no effect if we're talking to a remote server, but we can live with that.
if _pytest_options.webdriver == "firefox": if _pytest_options.webdriver == "firefox":
app.config[ "WEBDRIVER_PATH" ] = "geckodriver" app.config[ "WEBDRIVER_PATH" ] = shutil.which( "geckodriver" )
elif _pytest_options.webdriver == "chrome": elif _pytest_options.webdriver == "chrome":
app.config[ "WEBDRIVER_PATH" ] = "chromedriver" app.config[ "WEBDRIVER_PATH" ] = shutil.which( "chromedriver" )
return app return app
@ -209,15 +218,18 @@ def webdriver( request ):
# initialize # initialize
driver = request.config.getoption( "--webdriver" ) driver = request.config.getoption( "--webdriver" )
from selenium import webdriver as wb from selenium import webdriver as wb
log_fname = os.path.join( tempfile.gettempdir(), "webdriver-pytest.log" )
if driver == "firefox": if driver == "firefox":
service = wb.firefox.service.Service(
log_output = os.path.join( tempfile.gettempdir(), "webdriver-pytest.log" )
)
options = wb.FirefoxOptions() options = wb.FirefoxOptions()
options.headless = _pytest_options.headless if _pytest_options.headless:
service = wb.firefox.service.Service( log_path=log_fname ) options.add_argument( "--headless" )
driver = wb.Firefox( options=options, service=service ) driver = wb.Firefox( options=options, service=service )
elif driver == "chrome": elif driver == "chrome":
options = wb.ChromeOptions() options = wb.ChromeOptions()
options.headless = _pytest_options.headless if _pytest_options.headless:
options.add_argument( "--headless" )
options.add_argument( "--disable-gpu" ) options.add_argument( "--disable-gpu" )
driver = wb.Chrome( options=options ) driver = wb.Chrome( options=options )
else: else:

@ -2,5 +2,4 @@
IS_CONTAINER = 1 IS_CONTAINER = 1
JAVA_PATH = /usr/bin/jdk-15.0.1/bin/java
WEBDRIVER_PATH = /usr/bin/geckodriver WEBDRIVER_PATH = /usr/bin/geckodriver

@ -6,6 +6,10 @@ export DISPLAY=:10.0
Xvfb :10 -ac 1>/tmp/xvfb.log 2>/tmp/xvfb.err & Xvfb :10 -ac 1>/tmp/xvfb.log 2>/tmp/xvfb.err &
# run the webapp server # run the webapp server
python3 /app/vasl_templates/webapp/run_server.py \ # IMPORTANT! This script runs as PID 1, which is the only process that will receive signals,
# so we must replace it with the Python webserver process if it is to receive e.g. SIGTERM,
# which we must handle if "docker stop" and scaling down in Kubernetes is to work (otherwise
# things timeout and we get SIGKILL'ed).
exec python3 /app/vasl_templates/webapp/run_server.py \
--addr 0.0.0.0 \ --addr 0.0.0.0 \
--force-init-delay 30 --force-init-delay 30

@ -1,16 +1,12 @@
{ {
"SCENARIO_NAME": "Hill 621", "COMPASS": "right",
"SCENARIO_ID": "ASL E",
"SCENARIO_LOCATION": "Near Minsk, Russia",
"SCENARIO_DATE": "1944-07-01", "SCENARIO_DATE": "1944-07-01",
"SCENARIO_WIDTH": "", "SCENARIO_WIDTH": "",
"PLAYER_1_DESCRIPTION": "Retreating elements of 170th Infantry Division",
"PLAYER_2_DESCRIPTION": "Elements of 5th Guards Army",
"ASA_ID": "56512", "ASA_ID": "56512",
"ROAR_ID": "129", "ROAR_ID": "129",
"PLAYERS_WIDTH": "", "PLAYERS_WIDTH": "",
"VICTORY_CONDITIONS_WIDTH": "240px", "VICTORY_CONDITIONS_WIDTH": "",
"SSR_WIDTH": "500px", "SSR_WIDTH": "400px",
"OB_VEHICLES_WIDTH_1": "", "OB_VEHICLES_WIDTH_1": "",
"OB_VEHICLES_MA_NOTES_WIDTH_1": "300px", "OB_VEHICLES_MA_NOTES_WIDTH_1": "300px",
"OB_ORDNANCE_WIDTH_1": "", "OB_ORDNANCE_WIDTH_1": "",
@ -19,7 +15,12 @@
"OB_VEHICLES_MA_NOTES_WIDTH_2": "300px", "OB_VEHICLES_MA_NOTES_WIDTH_2": "300px",
"OB_ORDNANCE_WIDTH_2": "", "OB_ORDNANCE_WIDTH_2": "",
"OB_ORDNANCE_MA_NOTES_WIDTH_2": "300px", "OB_ORDNANCE_MA_NOTES_WIDTH_2": "300px",
"VICTORY_CONDITIONS": "The Russians win at Game End if they Control &ge; five Level 3 hill hexes on Board 2.", "SCENARIO_NAME": "Hill 621",
"SCENARIO_ID": "ASL E",
"SCENARIO_LOCATION": "Near Minsk, Russia",
"PLAYER_1_DESCRIPTION": "Retreating elements of 170th Infantry Division",
"PLAYER_2_DESCRIPTION": "Elements of 5th Guards Army",
"VICTORY_CONDITIONS": "The Russians win at Game End if they Control<br>≥ five Level 3 hill hexes on Board 2.",
"SCENARIO_THEATER": "ETO", "SCENARIO_THEATER": "ETO",
"PLAYER_1": "russian", "PLAYER_1": "russian",
"PLAYER_1_ELR": "4", "PLAYER_1_ELR": "4",
@ -27,6 +28,15 @@
"PLAYER_2": "german", "PLAYER_2": "german",
"PLAYER_2_ELR": "3", "PLAYER_2_ELR": "3",
"PLAYER_2_SAN": "4", "PLAYER_2_SAN": "4",
"TURN_TRACK": {
"NTURNS": "10",
"WIDTH": "",
"VERTICAL": false,
"SHADING": "",
"REINFORCEMENTS_1": "2,5",
"REINFORCEMENTS_2": "1,2,4,5,8",
"SWAP_PLAYERS": false
},
"SSR": [ "SSR": [
"EC are Moderate, with no wind at start.", "EC are Moderate, with no wind at start.",
"After \"At Start\" placement, each German infantry unit must take a TC. The only possible consequence of failure is that the unit must begin the scenario broken. Those units which break during this pre-game TC are not subject to DM in the initial German RPh.", "After \"At Start\" placement, each German infantry unit must take a TC. The only possible consequence of failure is that the unit must begin the scenario broken. Those units which break during this pre-game TC are not subject to DM in the initial German RPh.",
@ -106,7 +116,7 @@
], ],
"SCENARIO_NOTES": [ "SCENARIO_NOTES": [
{ {
"caption": "Download the scenario card from <a href=\"http://www.multimanpublishing.com/Support/ASLASLSK/ASLOfficialDownloads/tabid/109/Default.aspx\">Multi-Man Publishing</a> (ASL Classic pack).", "caption": "Download the scenario card from <a href=\"https://mmpgamers.com/asl-downloads-ezp-3#scenarios\">Multi-Man Publishing</a> (ASL Classic pack).",
"width": "300px", "width": "300px",
"id": 1 "id": 1
} }
@ -173,7 +183,7 @@
"id": 2 "id": 2
} }
], ],
"_app_version": "v1.3", "_app_version": "v1.10",
"_last_update_time": "2020-09-27T04:13:08.719Z", "_last_update_time": "2022-09-12T02:46:18.035Z",
"_creation_time": "2020-09-27T03:46:56.089Z" "_creation_time": "2020-09-27T03:46:56.089Z"
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 MiB

After

Width:  |  Height:  |  Size: 3.8 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 105 KiB

After

Width:  |  Height:  |  Size: 109 KiB

@ -1,15 +1,11 @@
{ {
"SCENARIO_NAME": "Hube's Pocket", "COMPASS": "down",
"SCENARIO_ID": "ASL G",
"SCENARIO_LOCATION": "Near Buchach, Southern Russia",
"SCENARIO_DATE": "1944-04-06", "SCENARIO_DATE": "1944-04-06",
"SCENARIO_WIDTH": "", "SCENARIO_WIDTH": "",
"PLAYER_1_DESCRIPTION": "Advance elements of 5th Tank Corps",
"PLAYER_2_DESCRIPTION": "10th SS Panzer Division \"Frundsberg\" and the First Panzer Army ",
"ASA_ID": "56514", "ASA_ID": "56514",
"ROAR_ID": "131", "ROAR_ID": "131",
"PLAYERS_WIDTH": "", "PLAYERS_WIDTH": "",
"VICTORY_CONDITIONS_WIDTH": "320px", "VICTORY_CONDITIONS_WIDTH": "",
"SSR_WIDTH": "330px", "SSR_WIDTH": "330px",
"OB_VEHICLES_WIDTH_1": "", "OB_VEHICLES_WIDTH_1": "",
"OB_VEHICLES_MA_NOTES_WIDTH_1": "300px", "OB_VEHICLES_MA_NOTES_WIDTH_1": "300px",
@ -19,7 +15,12 @@
"OB_VEHICLES_MA_NOTES_WIDTH_2": "300px", "OB_VEHICLES_MA_NOTES_WIDTH_2": "300px",
"OB_ORDNANCE_WIDTH_2": "", "OB_ORDNANCE_WIDTH_2": "",
"OB_ORDNANCE_MA_NOTES_WIDTH_2": "300px", "OB_ORDNANCE_MA_NOTES_WIDTH_2": "300px",
"VICTORY_CONDITIONS": "The Germans win immediately by exiting &ge; 10 vehicles <br>\noff the west edge in either one or two Convoys (see SSR 4).", "SCENARIO_NAME": "Hube's Pocket",
"SCENARIO_ID": "ASL G",
"SCENARIO_LOCATION": "Near Buchach, Southern Russia",
"PLAYER_1_DESCRIPTION": "Advance elements of 5th Tank Corps",
"PLAYER_2_DESCRIPTION": "10th SS Panzer Division \"Frundsberg\" and the First Panzer Army",
"VICTORY_CONDITIONS": "The Germans win immediately by exiting ≥ 10 vehicles <br>\noff the west edge in either one or two Convoys (see SSR 4).",
"SCENARIO_THEATER": "ETO", "SCENARIO_THEATER": "ETO",
"PLAYER_1": "german", "PLAYER_1": "german",
"PLAYER_1_ELR": "4", "PLAYER_1_ELR": "4",
@ -27,6 +28,15 @@
"PLAYER_2": "russian", "PLAYER_2": "russian",
"PLAYER_2_ELR": "3", "PLAYER_2_ELR": "3",
"PLAYER_2_SAN": "2", "PLAYER_2_SAN": "2",
"TURN_TRACK": {
"NTURNS": "14",
"WIDTH": "5",
"VERTICAL": false,
"SHADING": "",
"REINFORCEMENTS_1": "1,5",
"REINFORCEMENTS_2": "1",
"SWAP_PLAYERS": true
},
"SSR": [ "SSR": [
"The SPW 251/sMG inherent HS is a 3-4-8.", "The SPW 251/sMG inherent HS is a 3-4-8.",
"German inherent crews have a morale of 9.", "German inherent crews have a morale of 9.",
@ -84,7 +94,7 @@
], ],
"SCENARIO_NOTES": [ "SCENARIO_NOTES": [
{ {
"caption": "Download the scenario card from <a href=\"http://www.multimanpublishing.com/Support/ASLASLSK/ASLOfficialDownloads/tabid/109/Default.aspx\">Multi-Man Publishing</a> (ASL Classic pack).", "caption": "Download the scenario card from <a href=\"https://mmpgamers.com/asl-downloads-ezp-3#scenarios\">Multi-Man Publishing</a> (ASL Classic pack).",
"width": "", "width": "",
"id": 1 "id": 1
} }
@ -110,7 +120,7 @@
], ],
"OB_NOTES_1": [], "OB_NOTES_1": [],
"OB_NOTES_2": [], "OB_NOTES_2": [],
"_app_version": "v1.3", "_app_version": "v1.10",
"_last_update_time": "2020-09-27T04:33:18.643Z", "_last_update_time": "2022-09-12T02:22:47.511Z",
"_creation_time": "2020-09-27T04:11:07.200Z" "_creation_time": "2020-09-27T04:11:07.200Z"
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 MiB

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

After

Width:  |  Height:  |  Size: 79 KiB

@ -4,4 +4,4 @@ This directory contains examples of *VASL Templates* in action, with the `.json`
The online versions contain images that will be loaded from the internet, which looks much better, but there will be a short delay when you open the scenario in VASSAL as the images are downloaded. The online versions contain images that will be loaded from the internet, which looks much better, but there will be a short delay when you open the scenario in VASSAL as the images are downloaded.
These scenarios were taken from Multi-Man Publishing's [*ASL Classic* scenario pack](http://www.multimanpublishing.com/Support/ASLASLSK/ASLOfficialDownloads/tabid/109/Default.aspx). These scenarios were taken from Multi-Man Publishing's [*ASL Classic* scenario pack](https://mmpgamers.com/asl-downloads-ezp-3#scenarios).

@ -1,15 +1,11 @@
{ {
"SCENARIO_NAME": "The Streets Of Stalingrad", "COMPASS": "up",
"SCENARIO_ID": "ASL C",
"SCENARIO_LOCATION": "Stalingrad, Russia",
"SCENARIO_DATE": "1942-10-06", "SCENARIO_DATE": "1942-10-06",
"SCENARIO_WIDTH": "", "SCENARIO_WIDTH": "",
"PLAYER_1_DESCRIPTION": "308th Rifle Division / 295th Rifle Division / 2nd Battalion, 37th Guards Division",
"PLAYER_2_DESCRIPTION": "389th Infantry Division",
"ASA_ID": "56510", "ASA_ID": "56510",
"ROAR_ID": "127", "ROAR_ID": "127",
"PLAYERS_WIDTH": "", "PLAYERS_WIDTH": "",
"VICTORY_CONDITIONS_WIDTH": "400px", "VICTORY_CONDITIONS_WIDTH": "450px",
"SSR_WIDTH": "500px", "SSR_WIDTH": "500px",
"OB_VEHICLES_WIDTH_1": "", "OB_VEHICLES_WIDTH_1": "",
"OB_VEHICLES_MA_NOTES_WIDTH_1": "300px", "OB_VEHICLES_MA_NOTES_WIDTH_1": "300px",
@ -19,7 +15,12 @@
"OB_VEHICLES_MA_NOTES_WIDTH_2": "300px", "OB_VEHICLES_MA_NOTES_WIDTH_2": "300px",
"OB_ORDNANCE_WIDTH_2": "", "OB_ORDNANCE_WIDTH_2": "",
"OB_ORDNANCE_MA_NOTES_WIDTH_2": "300px", "OB_ORDNANCE_MA_NOTES_WIDTH_2": "300px",
"VICTORY_CONDITIONS": "Victory is based upon satisfying the Victory Conditions of Scenarios A and B:\n<ul style=\"margin:0 0 10px 10px;\">\n<li> If each side fulfills one Victory Condition, the game is a draw.\n<li> If a player fulfills one Victory Condition and draws the other, he wnis.\n<li> A decisive victory is achieved when a player fulfills both Victory Conditions.\n</ul>\n\n<p> <b>Scenario A:</b> The Russians win at Game End if they Control &ge; 2 more buildings initially occupied by the Germans than they lose of their own initially-held stone buildings to German Control, and/or have a favorable 3:1 ratio of unbroken squad-equivalents.\n<p> <b>Scenario B:</b> At Game End, the player with undisputed control of at least 6 hexes of building X3 wins. A hex containing a Melee is controlled by neither player. If only one player has an unbroken unit in the building at Game End, that player is the winner. Any other result is a draw.\n</ul>", "SCENARIO_NAME": "The Streets Of Stalingrad",
"SCENARIO_ID": "ASL C",
"SCENARIO_LOCATION": "Stalingrad, Russia",
"PLAYER_1_DESCRIPTION": "308th Rifle Division / 295th Rifle Division / 2nd Battalion, 37th Guards Division",
"PLAYER_2_DESCRIPTION": "389th Infantry Division",
"VICTORY_CONDITIONS": "Victory is based upon satisfying the Victory Conditions of Scenarios A and B:\n<ul style=\"margin:0 0 10px 10px;\">\n<li> If each side fulfills one Victory Condition, the game is a draw.\n</li><li> If a player fulfills one Victory Condition and draws the other, he wnis.\n</li><li> A decisive victory is achieved when a player fulfills both Victory Conditions.\n</li></ul>\n\n<p> <b>Scenario A:</b> The Russians win at Game End if they Control ≥ 2 more buildings initially occupied by the Germans than they lose of their own initially-held stone buildings to German Control, and/or have a favorable 3:1 ratio of unbroken squad-equivalents.\n</p><p> <b>Scenario B:</b> At Game End, the player with undisputed control of at least 6 hexes of building X3 wins. A hex containing a Melee is controlled by neither player. If only one player has an unbroken unit in the building at Game End, that player is the winner. Any other result is a draw.\n</p>",
"SCENARIO_THEATER": "ETO", "SCENARIO_THEATER": "ETO",
"PLAYER_1": "russian", "PLAYER_1": "russian",
"PLAYER_1_ELR": "3", "PLAYER_1_ELR": "3",
@ -27,6 +28,15 @@
"PLAYER_2": "german", "PLAYER_2": "german",
"PLAYER_2_ELR": "4", "PLAYER_2_ELR": "4",
"PLAYER_2_SAN": "6", "PLAYER_2_SAN": "6",
"TURN_TRACK": {
"NTURNS": "7",
"WIDTH": "",
"VERTICAL": false,
"SHADING": "",
"REINFORCEMENTS_1": "2",
"REINFORCEMENTS_2": "3",
"SWAP_PLAYERS": false
},
"SSR": [ "SSR": [
"Roll a die to determine who moves first.", "Roll a die to determine who moves first.",
"Set up the forces of Scenario A prior to placing the units of Scenario B.", "Set up the forces of Scenario A prior to placing the units of Scenario B.",
@ -61,7 +71,7 @@
], ],
"SCENARIO_NOTES": [ "SCENARIO_NOTES": [
{ {
"caption": "Download the scenario card from <a href=\"http://www.multimanpublishing.com/Support/ASLASLSK/ASLOfficialDownloads/tabid/109/Default.aspx\">Multi-Man Publishing</a> (ASL Classic pack).", "caption": "Download the scenario card from <a href=\"https://mmpgamers.com/asl-downloads-ezp-3#scenarios\">Multi-Man Publishing</a> (ASL Classic pack).",
"width": "", "width": "",
"id": 1 "id": 1
} }
@ -157,7 +167,7 @@
], ],
"OB_NOTES_1": [], "OB_NOTES_1": [],
"OB_NOTES_2": [], "OB_NOTES_2": [],
"_app_version": "v1.3", "_app_version": "v1.10",
"_last_update_time": "2020-09-27T04:46:21.803Z", "_last_update_time": "2022-09-12T02:58:13.237Z",
"_creation_time": "2020-09-27T04:44:48.473Z" "_creation_time": "2020-09-27T04:44:48.473Z"
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

After

Width:  |  Height:  |  Size: 902 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 61 KiB

@ -95,8 +95,8 @@ def main( args ): #pylint: disable=too-many-locals
args.append( "--noconsole" ) args.append( "--noconsole" )
args.extend( [ "--icon", APP_ICON ] ) args.extend( [ "--icon", APP_ICON ] )
# NOTE: These files are not always required but it's probably safer to always include them. # NOTE: These files are not always required but it's probably safer to always include them.
import distutils.sysconfig #pylint: disable=import-error import sysconfig
dname = os.path.join( distutils.sysconfig.get_python_lib() , "PyQt5/Qt5/bin" ) dname = os.path.join( sysconfig.get_path("platlib") , "PyQt5/Qt5/bin" )
args.extend( [ "--add-binary", os.path.join(dname,"libEGL.dll") + os.pathsep + "PyQt5/Qt/bin" ] ) args.extend( [ "--add-binary", os.path.join(dname,"libEGL.dll") + os.pathsep + "PyQt5/Qt/bin" ] )
args.extend( [ "--add-binary", os.path.join(dname,"libGLESv2.dll") + os.pathsep + "PyQt5/Qt/bin" ] ) args.extend( [ "--add-binary", os.path.join(dname,"libGLESv2.dll") + os.pathsep + "PyQt5/Qt/bin" ] )
args.append( MAIN_SCRIPT ) args.append( MAIN_SCRIPT )

@ -98,7 +98,7 @@ def _convert_app_icon( save_fname ):
# to an image, then insert it into the PyInstaller-generated executable (so that # to an image, then insert it into the PyInstaller-generated executable (so that
# we don't have to bundle Pillow into the release). # we don't have to bundle Pillow into the release).
img = Image.open( APP_ICON ) img = Image.open( APP_ICON )
img = img.convert( "RGBA" ).resize( (64, 64) ) img = img.convert( "RGBA" ).resize( (48, 48) )
img.save( save_fname, "png" ) img.save( save_fname, "png" )
# --------------------------------------------------------------------- # ---------------------------------------------------------------------

@ -93,7 +93,7 @@ def create_window( app_icon ):
"""Create the splash window.""" """Create the splash window."""
# create the splash window # create the splash window
main_window.geometry( "290x75" ) main_window.geometry( "275x64" )
main_window.title( "vasl-templates loader" ) main_window.title( "vasl-templates loader" )
main_window.overrideredirect( 1 ) # nb: "-type splash" doesn't work on Windows :-/ main_window.overrideredirect( 1 ) # nb: "-type splash" doesn't work on Windows :-/
main_window.eval( "tk::PlaceWindow . center" ) main_window.eval( "tk::PlaceWindow . center" )
@ -103,11 +103,11 @@ def create_window( app_icon ):
# add the app icon # add the app icon
label = tkinter.Label( main_window, image=app_icon ) label = tkinter.Label( main_window, image=app_icon )
label.grid( row=0, column=0, rowspan=2, padx=5, pady=5 ) label.grid( row=0, column=0, rowspan=2, padx=8, pady=8 )
# add the caption # add the caption
label = tkinter.Label( main_window, text="Loading vasl-templates...", font=("Helvetica",12) ) label = tkinter.Label( main_window, text="Loading vasl-templates...", font=("Helvetica",12) )
label.grid( row=0, column=1, padx=5, pady=(5,0) ) label.grid( row=0, column=1, padx=5, pady=(8,0) )
# add the "loading" image (we have to animate it ourself :-/) # add the "loading" image (we have to animate it ourself :-/)
anim_label = tkinter.Label( main_window ) anim_label = tkinter.Label( main_window )

@ -1,7 +1,7 @@
pytest==6.2.5 pytest==7.4.2
grpcio-tools==1.44.0 grpcio-tools==1.58.0
tabulate==0.8.9 tabulate==0.9.0
lxml==4.8.0 lxml==4.9.3
pylint==2.12.2 pylint==2.17.5
pytest-pylint==0.18.0 pytest-pylint==0.19.0
pyinstaller==4.9 pyinstaller==5.13.2

@ -1,8 +1,10 @@
# python 3.8.7 # python 3.11.4
flask==2.0.3 flask==2.3.3
pyyaml==6.0 pyyaml==6.0.1
pillow==9.0.1 # NOTE: Pillow 9.5.0 is the last version that provides 32-bit wheels.
selenium==4.1.0 pillow==9.5.0
waitress==2.0.0 selenium==4.12.0
click==8.0.4 waitress==2.1.2
appdirs==1.4.4
click==8.1.7

@ -14,6 +14,11 @@ function main
VASL_BOARDS= VASL_BOARDS=
CHAPTER_H_NOTES= CHAPTER_H_NOTES=
USER_FILES= USER_FILES=
LOGGING_CONFIG=
VASSAL_SHIM_LOGGING_CONFIG=
ASA_INDEX=
ROAR_INDEX=
VO_NOTES_IMAGE_CACHE=
TEMPLATE_PACK= TEMPLATE_PACK=
IMAGE_TAG=latest IMAGE_TAG=latest
CONTAINER_NAME=vasl-templates CONTAINER_NAME=vasl-templates
@ -31,7 +36,7 @@ function main
print_help print_help
exit 0 exit 0
fi fi
params="$(getopt -o p:v:e:k:t:d -l port:,control-tests-port:,vassal:,vasl:,vasl-extensions:,boards:,chapter-h:,template-pack:,user-files:,tag:,name:,detach,no-build,build-arg:,build-network:,run-network:,test-data-vassal:,test-data-vasl-mods:,help --name "$0" -- "$@")" params="$(getopt -o p:v:e:k:t:d -l port:,control-tests-port:,vassal:,vasl:,vasl-extensions:,boards:,chapter-h:,template-pack:,user-files:,logging:,vassal-shim-logging:,asa-index:,roar-index:,vo-notes-image-cache:,tag:,name:,detach,no-build,build-arg:,build-network:,run-network:,test-data-vassal:,test-data-vasl-mods:,help --name "$0" -- "$@")"
if [ $? -ne 0 ]; then exit 1; fi if [ $? -ne 0 ]; then exit 1; fi
eval set -- "$params" eval set -- "$params"
while true; do while true; do
@ -60,6 +65,21 @@ function main
-k | --template-pack) -k | --template-pack)
TEMPLATE_PACK=$2 TEMPLATE_PACK=$2
shift 2 ;; shift 2 ;;
--logging)
LOGGING_CONFIG=$2
shift 2 ;;
--vassal-shim-logging)
VASSAL_SHIM_LOGGING_CONFIG=$2
shift 2 ;;
--asa-index)
ASA_INDEX=$2
shift 2 ;;
--roar-index)
ROAR_INDEX=$2
shift 2 ;;
--vo-notes-image-cache)
VO_NOTES_IMAGE_CACHE=$2
shift 2 ;;
-t | --tag) -t | --tag)
IMAGE_TAG=$2 IMAGE_TAG=$2
shift 2 ;; shift 2 ;;
@ -186,7 +206,67 @@ function main
fi fi
mpoint=/data/template-pack mpoint=/data/template-pack
TEMPLATE_PACK_VOLUME="--volume $target:$mpoint" TEMPLATE_PACK_VOLUME="--volume $target:$mpoint"
TEMPLATE_PACK_ENV="--env DEFAULT_TEMPLATE_PACK=$mpoint --env DEFAULT_TEMPLATE_PACK_TARGET" TEMPLATE_PACK_ENV="--env DEFAULT_TEMPLATE_PACK=$mpoint"
fi
# check if logging has been configured
if [ -n "$LOGGING_CONFIG" ]; then
target=$( get_target FILE "$LOGGING_CONFIG" )
if [ -z "$target" ]; then
echo "Can't find the logging config file: $LOGGING_CONFIG"
exit 1
fi
mpoint=/app/vasl_templates/webapp/config/logging.yaml
LOGGING_CONFIG_VOLUME="--volume $target:$mpoint"
fi
if [ -n "$VASSAL_SHIM_LOGGING_CONFIG" ]; then
target=$( get_target FILE "$VASSAL_SHIM_LOGGING_CONFIG" )
if [ -z "$target" ]; then
echo "Can't find the VASSAL shim logging config file: $VASSAL_SHIM_LOGGING_CONFIG"
exit 1
fi
mpoint=/app/vassal-shim/release/logback-test.xml
VASSAL_SHIM_LOGGING_CONFIG_VOLUME="--volume $target:$mpoint"
fi
# check if external ASA/ROAR index files have been specified
# NOTE: We don't need to pass env.vars into the container, or anything like that. The code already
# saves the downloaded files in /tmp/ (inside the container), so all we need to do is map these files
# to the specified external files.
if [ -n "$ASA_INDEX" ]; then
target=$( realpath --no-symlinks "$ASA_INDEX" )
if [ ! -f "$target" ]; then
if ! touch "$target" 2>/dev/null; then
echo "Can't find the ASA index file: $ASA_INDEX"
exit 1
fi
fi
mpoint=/tmp/asl-scenario-archive.json
ASA_INDEX_VOLUME="--volume $target:$mpoint"
fi
if [ -n "$ROAR_INDEX" ]; then
target=$( realpath --no-symlinks "$ROAR_INDEX" )
if [ ! -f "$target" ]; then
if ! touch "$target" 2>/dev/null ; then
echo "Can't find the ROAR index file: $ASA_INDEX"
exit 1
fi
fi
mpoint=/tmp/roar-scenario-index.json
ROAR_INDEX_VOLUME="--volume $target:$mpoint"
fi
# check if an external v/o notes image cache directory has been specified
if [ -n "$VO_NOTES_IMAGE_CACHE" ]; then
target=$( realpath --no-symlinks "$VO_NOTES_IMAGE_CACHE" )
if [ ! -d "$target" ]; then
if ! mkdir "$target" 2>/dev/null; then
echo "Can't find the V/O notes image cache directory: $VO_NOTES_IMAGE_CACHE"
exit 1
fi
fi
mpoint=/tmp/vo-notes-image-cache/
VO_NOTES_IMAGE_CACHE_VOLUME="--volume $target:$mpoint"
fi fi
# check if testing has been enabled # check if testing has been enabled
@ -219,6 +299,8 @@ function main
--env DOCKER_CONTAINER_NAME="$CONTAINER_NAME" \ --env DOCKER_CONTAINER_NAME="$CONTAINER_NAME" \
$CONTROL_TESTS_PORT_RUN \ $CONTROL_TESTS_PORT_RUN \
$VASSAL_VOLUME $VASL_MOD_VOLUME $VASL_EXTNS_VOLUME $VASL_BOARDS_VOLUME $CHAPTER_H_NOTES_VOLUME $TEMPLATE_PACK_VOLUME $USER_FILES_VOLUME \ $VASSAL_VOLUME $VASL_MOD_VOLUME $VASL_EXTNS_VOLUME $VASL_BOARDS_VOLUME $CHAPTER_H_NOTES_VOLUME $TEMPLATE_PACK_VOLUME $USER_FILES_VOLUME \
$LOGGING_CONFIG_VOLUME $VASSAL_SHIM_LOGGING_CONFIG_VOLUME \
$ASA_INDEX_VOLUME $ROAR_INDEX_VOLUME $VO_NOTES_IMAGE_CACHE_VOLUME \
$VASSAL_ENV $VASL_MOD_ENV $VASL_EXTNS_ENV $VASL_BOARDS_ENV $CHAPTER_H_NOTES_ENV $TEMPLATE_PACK_ENV $USER_FILES_ENV \ $VASSAL_ENV $VASL_MOD_ENV $VASL_EXTNS_ENV $VASL_BOARDS_ENV $CHAPTER_H_NOTES_ENV $TEMPLATE_PACK_ENV $USER_FILES_ENV \
$RUN_NETWORK $DETACH \ $RUN_NETWORK $DETACH \
$TEST_DATA_VASSAL $TEST_DATA_VASL_MODS \ $TEST_DATA_VASSAL $TEST_DATA_VASL_MODS \
@ -288,10 +370,15 @@ function print_help {
--build-network Docker network to use when building the image. --build-network Docker network to use when building the image.
--run-network Docker network to use when running the container. --run-network Docker network to use when running the container.
Options for storing data files outside the container (so that they can be re-used):
--asa-index Path to the ASL Scenario Archive index file (downloaded).
--roar-index Path to the ROAR index file (downloaded).
--vo-notes-image-cache Cache directory for images generated for vehicle/ordnance notes.
Options for the test suite: Options for the test suite:
--control-tests-port Remote test control port number. --control-tests-port Remote test control port number.
--test-data-vassal Directory containing VASSAL releases. --test-data-vassal Directory containing VASSAL releases.
--test-data-vasl-mods Directory containing VASL modules. --test-data-vasl-mods Directory containing VASL modules.
NOTE: If the port the webapp server is listening on *inside* the container is different NOTE: If the port the webapp server is listening on *inside* the container is different
to the port exposed *outside* the container, webdriver image generation (e.g. Shift-Click to the port exposed *outside* the container, webdriver image generation (e.g. Shift-Click

@ -28,10 +28,10 @@ def parse_requirements( fname ):
setup( setup(
name = "vasl_templates", name = "vasl_templates",
version = "1.9", # nb: also update constants.py version = "1.13", # nb: also update constants.py
description = "Create HTML snippets for use in VASL.", description = "Create HTML snippets for use in VASL.",
license = "AGPLv3", license = "AGPLv3",
url = "https://github.com/pacman-ghost/vasl-templates", url = "https://code.pacman-ghost.com/public/vasl-templates",
packages = find_packages(), packages = find_packages(),
install_requires = parse_requirements( "requirements.txt" ), install_requires = parse_requirements( "requirements.txt" ),
extras_require = { extras_require = {

@ -11,7 +11,7 @@ from PyQt5.QtCore import QUrl
from PyQt5.QtGui import QDesktopServices, QIcon, QCursor from PyQt5.QtGui import QDesktopServices, QIcon, QCursor
from PyQt5.QtWidgets import QDialog from PyQt5.QtWidgets import QDialog
from vasl_templates.webapp.config.constants import APP_NAME, APP_VERSION, APP_HOME_URL, IS_FROZEN from vasl_templates.webapp.config.constants import APP_NAME, APP_VERSION, APP_HOME_URL, APP_ISSUES_URL, IS_FROZEN
from vasl_templates.utils import get_build_info from vasl_templates.utils import get_build_info
# --------------------------------------------------------------------- # ---------------------------------------------------------------------
@ -51,7 +51,7 @@ class AboutDialog( QDialog ):
time.strftime( "%d %B %Y %H:%M", time.localtime( build_info["timestamp"] ) ) time.strftime( "%d %B %Y %H:%M", time.localtime( build_info["timestamp"] ) )
) ) ) )
if "git_info" in build_info: if "git_info" in build_info:
buf.write( " <small><em>({})</em></small>".format( build_info["git_info"] ) ) buf.write( " <small><tt>({})</tt></small>".format( build_info["git_info"] ) )
buf.write( "." ) buf.write( "." )
self.build_info.setText( buf.getvalue() ) self.build_info.setText( buf.getvalue() )
else: else:
@ -60,8 +60,11 @@ class AboutDialog( QDialog ):
self.home_url.setText( "Visit us at <a href='{}'>{}</a>.".format( self.home_url.setText( "Visit us at <a href='{}'>{}</a>.".format(
APP_HOME_URL, mo.group(1) if mo else APP_HOME_URL APP_HOME_URL, mo.group(1) if mo else APP_HOME_URL
) ) ) )
self.issues_url.setText( "Report a bug, request a feature, ask a question <a href='{}'>here</a>.".format(
APP_ISSUES_URL
) )
def on_app_icon_clicked( self, event ): #pylint: disable=no-self-use,unused-argument def on_app_icon_clicked( self, event ): #pylint: disable=unused-argument
"""Click handler.""" """Click handler."""
QDesktopServices.openUrl( QUrl( APP_HOME_URL ) ) QDesktopServices.openUrl( QUrl( APP_HOME_URL ) )

@ -21,6 +21,9 @@ class FileDialog:
self.default_extn = default_extn self.default_extn = default_extn
self.filters = filters self.filters = filters
self.curr_fname = default_fname self.curr_fname = default_fname
# NOTE: We can't just use the directory of self.curr_fname, since this gets reset
# when the user chooses "new scenario", but we want to remember the current directory.
self._curr_dir = os.path.dirname( default_fname ) if default_fname else None
def load_file( self, binary ): def load_file( self, binary ):
"""Load a file.""" """Load a file."""
@ -28,7 +31,7 @@ class FileDialog:
# ask the user which file to load # ask the user which file to load
fname, _ = QFileDialog.getOpenFileName( fname, _ = QFileDialog.getOpenFileName(
self.parent, "Load {}".format( self.object_name ), self.parent, "Load {}".format( self.object_name ),
self.curr_fname, self._get_start_path(),
self.filters self.filters
) )
if not fname: if not fname:
@ -44,6 +47,7 @@ class FileDialog:
if not binary: if not binary:
data = data.decode( "utf-8" ) data = data.decode( "utf-8" )
self.curr_fname = fname self.curr_fname = fname
self._curr_dir = os.path.dirname( fname )
return data return data
@ -58,8 +62,8 @@ class FileDialog:
# ask the user where to save the file # ask the user where to save the file
fname, _ = QFileDialog.getSaveFileName( fname, _ = QFileDialog.getSaveFileName(
self.parent, "Save {}".format( self.object_name), self.parent, "Save {}".format( self.object_name ),
self.curr_fname, self._get_start_path(),
self.filters self.filters
) )
if not fname: if not fname:
@ -80,4 +84,13 @@ class FileDialog:
self.parent.showErrorMsg( "Can't save the {}:\n\n{}".format( self.object_name, ex ) ) self.parent.showErrorMsg( "Can't save the {}:\n\n{}".format( self.object_name, ex ) )
continue continue
self.curr_fname = fname self.curr_fname = fname
self._curr_dir = os.path.dirname( fname )
return True return True
def _get_start_path( self ):
"""Get the start filename or directory path for saving/loading files."""
if self.curr_fname and os.path.isabs( self.curr_fname ):
return self.curr_fname
if self._curr_dir and self.curr_fname:
return os.path.join( self._curr_dir, self.curr_fname )
return self.curr_fname

@ -22,6 +22,10 @@ from PyQt5.QtCore import Qt, QSettings, QDir
import PyQt5.QtCore import PyQt5.QtCore
import click import click
# notify everyone that we're being run as the desktop application
os.environ[ "IS_DESKTOP_APP" ] = "1"
os.environ[ "QDIR_HOME_PATH" ] = QDir.homePath()
from vasl_templates.webapp.utils import SimpleError, is_windows from vasl_templates.webapp.utils import SimpleError, is_windows
# NOTE: We're supposed to do the following to support HiDPI, but it causes the main window # NOTE: We're supposed to do the following to support HiDPI, but it causes the main window
@ -95,9 +99,20 @@ def _do_main( template_pack, default_scenario, remote_debugging, debug ): #pylin
# NOTE: We do these imports here (instead of at the top of the file) so that we can catch errors. # NOTE: We do these imports here (instead of at the top of the file) so that we can catch errors.
from vasl_templates.webapp import app as webapp from vasl_templates.webapp import app as webapp
from vasl_templates.webapp import load_debug_config from vasl_templates.webapp import globvars, load_debug_config
from vasl_templates.webapp import main as webapp_main, snippets as webapp_snippets from vasl_templates.webapp import main as webapp_main, snippets as webapp_snippets
# initialize logging
# NOTE: We set up basic logging for people using the desktop app (if they are running from source,
# or using Docker, there is an expectation they can do this themselves). If logging has already
# been set up, this config is in *addition* to what's already been configured.
handler = logging.FileHandler( globvars.user_profile.default_log_fname, mode="w" )
handler.setLevel( logging.WARNING )
handler.setFormatter(
logging.Formatter( "%(asctime)s | %(message)s" )
)
logging.getLogger().addHandler( handler )
# configure the default template pack # configure the default template pack
if template_pack: if template_pack:
if template_pack.lower().endswith( ".zip" ): if template_pack.lower().endswith( ".zip" ):
@ -121,12 +136,12 @@ def _do_main( template_pack, default_scenario, remote_debugging, debug ): #pylin
os.environ["QTWEBENGINE_REMOTE_DEBUGGING"] = remote_debugging os.environ["QTWEBENGINE_REMOTE_DEBUGGING"] = remote_debugging
# load the application settings # load the application settings
app_settings_fname = "vasl-templates.ini" if sys.platform == "win32" else ".vasl-templates.conf"
if not os.path.isfile( app_settings_fname ) :
app_settings_fname = os.path.join( QDir.homePath(), app_settings_fname )
# FUDGE! Declaring app_settings as global here doesn't work on Windows (?!), we have to do this weird import :-/ # FUDGE! Declaring app_settings as global here doesn't work on Windows (?!), we have to do this weird import :-/
import vasl_templates.main #pylint: disable=import-self import vasl_templates.main #pylint: disable=import-self
vasl_templates.main.app_settings = QSettings( app_settings_fname, QSettings.IniFormat ) vasl_templates.main.app_settings = QSettings(
globvars.user_profile.desktop_settings_fname,
QSettings.IniFormat
)
# install the debug config file # install the debug config file
if debug: if debug:
@ -228,6 +243,9 @@ def _do_main( template_pack, default_scenario, remote_debugging, debug ): #pylin
if is_windows(): if is_windows():
QApplication.setStyle( "windowsvista" ) QApplication.setStyle( "windowsvista" )
# disable the context help button in the title bar (Windows only)
QApplication.setAttribute( PyQt5.QtCore.Qt.AA_DisableWindowContextHelpButton )
# check if we should disable the embedded browser # check if we should disable the embedded browser
disable_browser = webapp.config.get( "DISABLE_WEBENGINEVIEW" ) disable_browser = webapp.config.get( "DISABLE_WEBENGINEVIEW" )

@ -14,9 +14,10 @@ from PyQt5.QtGui import QDesktopServices, QIcon
from PyQt5.QtCore import Qt, QUrl, QMargins, pyqtSlot, QVariant from PyQt5.QtCore import Qt, QUrl, QMargins, pyqtSlot, QVariant
from vasl_templates.webapp.config.constants import APP_NAME, APP_VERSION, IS_FROZEN from vasl_templates.webapp.config.constants import APP_NAME, APP_VERSION, IS_FROZEN
from vasl_templates.webapp import globvars
from vasl_templates.main import app_settings from vasl_templates.main import app_settings
from vasl_templates.web_channel import WebChannelHandler from vasl_templates.web_channel import WebChannelHandler
from vasl_templates.utils import catch_exceptions from vasl_templates.utils import catch_exceptions, launch_file
_CONSOLE_SOURCE_REGEX = re.compile( r"^http://.+?/static/(.*)$" ) _CONSOLE_SOURCE_REGEX = re.compile( r"^http://.+?/static/(.*)$" )
@ -25,7 +26,7 @@ _CONSOLE_SOURCE_REGEX = re.compile( r"^http://.+?/static/(.*)$" )
class AppWebPage( QWebEnginePage ): class AppWebPage( QWebEnginePage ):
"""Application web page.""" """Application web page."""
def acceptNavigationRequest( self, url, nav_type, is_mainframe ): #pylint: disable=no-self-use,unused-argument def acceptNavigationRequest( self, url, nav_type, is_mainframe ): #pylint: disable=unused-argument
"""Called when a link is clicked.""" """Called when a link is clicked."""
if url.host() in ("localhost","127.0.0.1"): if url.host() in ("localhost","127.0.0.1"):
if "/asl-rulebook2/" not in url.url(): # nb: asl-rulebook2 links are routed through our webapp if "/asl-rulebook2/" not in url.url(): # nb: asl-rulebook2 links are routed through our webapp
@ -39,12 +40,12 @@ class AppWebPage( QWebEnginePage ):
QDesktopServices.openUrl( url ) QDesktopServices.openUrl( url )
return False return False
def javaScriptConsoleMessage( self, level, msg, line_no, source_id ): #pylint: disable=unused-argument,no-self-use def javaScriptConsoleMessage( self, level, msg, line_no, source_id ): #pylint: disable=unused-argument
"""Log a Javascript console message.""" """Log a Javascript console message."""
mo = _CONSOLE_SOURCE_REGEX.search( source_id ) mo = _CONSOLE_SOURCE_REGEX.search( source_id )
source = mo.group(1) if mo else source_id source = mo.group(1) if mo else source_id
logger = logging.getLogger( "javascript" ) logger = logging.getLogger( "javascript" )
logger.info( "%s:%d - %s", source, line_no, msg ) logger.warning( "%s:%d - %s", source, line_no, msg )
# --------------------------------------------------------------------- # ---------------------------------------------------------------------
@ -80,6 +81,7 @@ class MainWindow( QWidget ):
action.triggered.connect( handler ) action.triggered.connect( handler )
file_menu.addAction( action ) file_menu.addAction( action )
add_action( "&Settings", "settings.png", self.on_settings ) add_action( "&Settings", "settings.png", self.on_settings )
add_action( "&Log file", "log.png", self.on_log_file )
add_action( "&About", "info.png", self.on_about ) add_action( "&About", "info.png", self.on_about )
file_menu.addSeparator() file_menu.addSeparator()
add_action( "E&xit", "exit.png", self.on_exit ) add_action( "E&xit", "exit.png", self.on_exit )
@ -94,7 +96,10 @@ class MainWindow( QWidget ):
self.restoreGeometry( val ) self.restoreGeometry( val )
else : else :
self.resize( 1000, 650 ) self.resize( 1000, 650 )
self.setMinimumSize( 1000, 620 ) # NOTE: This should be wide enough for the sortable hints to not wrap (so that
# we don't see a scrollbar when their panels are reduced to their minimum height).
# We also want the Trumbowyg button pane for the VC to wrap somewhere sensible.
self.setMinimumSize( 1030, 650 )
# initialize the layout # initialize the layout
layout = QVBoxLayout( self ) layout = QVBoxLayout( self )
@ -216,6 +221,10 @@ class MainWindow( QWidget ):
dlg = ServerSettingsDialog( self ) dlg = ServerSettingsDialog( self )
dlg.exec_() dlg.exec_()
def on_log_file( self ):
"""Menu action handler."""
launch_file( globvars.user_profile.default_log_fname )
def on_about( self ): def on_about( self ):
"""Menu action handler.""" """Menu action handler."""
from vasl_templates.about import AboutDialog #pylint: disable=cyclic-import from vasl_templates.about import AboutDialog #pylint: disable=cyclic-import
@ -308,7 +317,7 @@ class MainWindow( QWidget ):
@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 ):
"""Called when the user changes the user settings.""" """Called when the user changes the user settings."""
# delete all existing keys # delete all existing keys
for key in app_settings.allKeys(): for key in app_settings.allKeys():

@ -155,9 +155,9 @@ class ServerSettingsDialog( QDialog ):
app_settings.setValue( "ServerSettings/"+key, val ) app_settings.setValue( "ServerSettings/"+key, val )
try: try:
install_server_settings( False ) install_server_settings( False )
except Exception as ex: #pylint: disable=broad-except except Exception as ex2: #pylint: disable=broad-except
logging.error( traceback.format_exc() ) logging.error( traceback.format_exc() )
MainWindow.showErrorMsg( "Couldn't rollback the server settings:\n\n{}".format( ex ) ) MainWindow.showErrorMsg( "Couldn't rollback the server settings:\n\n{}".format( ex2 ) )
return return
self.close() self.close()

@ -0,0 +1,83 @@
#!/usr/bin/env python3
""" Prepare the piece info for a VASL module.
The main program used to identify 5/8" counters by reading a module's buildFile and checking the height
attribute of the PieceSlot nodes, but it turns out this is the wrong thing to do (this field actually
controls the size of the piece's entry in the counter palette):
https://github.com/vasl-developers/vasl/issues/1195
For each version of VASL supported, run vassal-shim (getPieceInfo command) to analyze the module's
buildFile and get the correct counter sizes. Then pass the output into this script, to generate
the final data file that should be saved in the $/data/vasl-$VERSION/ directory, where it will
be read by the main program.
NOTE: Introducing this process opens the possibility of also extracting the image file paths
within the .vmod file, instead of the current messy parsing of the PieceSlot CDATA... :-/
"""
import sys
import os
import json
import xml.etree.ElementTree as ET
# ---------------------------------------------------------------------
# initialize
report = {}
# figure out which GPID's we're interested in
gpids = set()
def get_gpids( vo_type ):
"""Get the GPID's from our data files."""
dname = os.path.join( os.path.dirname(__file__), "../webapp/data", vo_type )
for root,_,fnames in os.walk( dname ):
for fname in fnames:
if os.path.splitext( fname )[1] != ".json":
continue
fname = os.path.join( root, fname )
with open( fname, "r", encoding="utf-8" ) as fp:
entries = json.load( fp )
for entry in entries:
entry_gpid = entry.get( "gpid" )
if not entry_gpid:
continue
if isinstance( entry_gpid, list ):
gpids.update( str(g) for g in entry_gpid )
else:
gpids.add( str( entry_gpid ) )
get_gpids( "vehicles" )
get_gpids( "ordnance" )
# parse the piece info generated by vassal-shim
doc = ET.parse( sys.stdin )
for piece_info in doc.getroot():
gpid = piece_info.attrib["gpid"]
if gpid not in gpids:
continue
info = {}
# check if the next piece is small
# FUDGE! We used to check for <= 48, but what we get is GamePiece.boundingBox(), which is
# the click zone for the counter, not the actual size of the counter's image :-/
if int( piece_info.attrib["height"] ) <= 55:
info["is_small"] = True
if info:
report[ gpid ] = info
# FUDGE! These are from extensions - it's not worth trying to figure these out programtically.
report[ "adf:1948" ] = { "is_small": True } # BFP Blood & Jungle: Dutch Brandt 47mm Mortar
report[ "adf:75" ] = { "is_small": True } # BFP Blood & Jungle: Indonesian Type 89 Heavy Grenade Launcher
report[ "adf:77" ] = { "is_small": True } # BFP Blood & Jungle: Indonesian Type 97 Automatic Gun
report[ "adf:76" ] = { "is_small": True } # BFP Blood & Jungle: Indonesian Year-11 Flat-Trajectory INF Gun
report[ "adf:1407" ] = { "is_small": True } # BFP Poland In Flames: German 2cm Tankbusche S-18
report[ "08d:75" ] = { "is_small": True } # Fight For Seoul: American M20(L) 75mm Recoilless Rifle
# output the final report
print( "{" )
lines = []
for gpid, piece_info in report.items():
lines.append( "\"{}\": {}".format(
gpid, json.dumps( piece_info )
) )
print( ",\n".join( lines ) )
print( "}" )

@ -10,7 +10,7 @@
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>460</width> <width>460</width>
<height>182</height> <height>215</height>
</rect> </rect>
</property> </property>
<property name="windowTitle"> <property name="windowTitle">
@ -22,7 +22,7 @@
<widget class="QLabel" name="app_icon"> <widget class="QLabel" name="app_icon">
<property name="geometry"> <property name="geometry">
<rect> <rect>
<x>10</x> <x>15</x>
<y>10</y> <y>10</y>
<width>64</width> <width>64</width>
<height>64</height> <height>64</height>
@ -39,9 +39,9 @@
<property name="geometry"> <property name="geometry">
<rect> <rect>
<x>80</x> <x>80</x>
<y>20</y> <y>19</y>
<width>371</width> <width>361</width>
<height>61</height> <height>58</height>
</rect> </rect>
</property> </property>
<layout class="QVBoxLayout" name="verticalLayout"> <layout class="QVBoxLayout" name="verticalLayout">
@ -75,7 +75,6 @@
<property name="font"> <property name="font">
<font> <font>
<family>DejaVu Sans</family> <family>DejaVu Sans</family>
<italic>true</italic>
</font> </font>
</property> </property>
<property name="text"> <property name="text">
@ -88,9 +87,9 @@
<widget class="QWidget" name="horizontalLayoutWidget"> <widget class="QWidget" name="horizontalLayoutWidget">
<property name="geometry"> <property name="geometry">
<rect> <rect>
<x>10</x> <x>19</x>
<y>140</y> <y>172</y>
<width>441</width> <width>425</width>
<height>31</height> <height>31</height>
</rect> </rect>
</property> </property>
@ -129,9 +128,9 @@
<widget class="QLabel" name="home_url"> <widget class="QLabel" name="home_url">
<property name="geometry"> <property name="geometry">
<rect> <rect>
<x>10</x> <x>80</x>
<y>120</y> <y>122</y>
<width>441</width> <width>361</width>
<height>21</height> <height>21</height>
</rect> </rect>
</property> </property>
@ -148,12 +147,34 @@
<bool>true</bool> <bool>true</bool>
</property> </property>
</widget> </widget>
<widget class="QLabel" name="issues_url">
<property name="geometry">
<rect>
<x>80</x>
<y>142</y>
<width>361</width>
<height>21</height>
</rect>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>*** ISSUES URL ***</string>
</property>
<property name="openExternalLinks">
<bool>true</bool>
</property>
</widget>
<widget class="QLabel" name="license"> <widget class="QLabel" name="license">
<property name="geometry"> <property name="geometry">
<rect> <rect>
<x>10</x> <x>80</x>
<y>100</y> <y>100</y>
<width>441</width> <width>361</width>
<height>17</height> <height>17</height>
</rect> </rect>
</property> </property>

@ -509,7 +509,7 @@
<rect> <rect>
<x>650</x> <x>650</x>
<y>230</y> <y>230</y>
<width>164</width> <width>173</width>
<height>31</height> <height>31</height>
</rect> </rect>
</property> </property>

@ -1,12 +1,15 @@
""" Miscellaneous utilities. """ """ Miscellaneous utilities. """
import os import os
import platform
import subprocess
import functools import functools
import logging import logging
import traceback import traceback
import json import json
from vasl_templates.webapp.config.constants import BASE_DIR from vasl_templates.webapp import app
from vasl_templates.webapp.config.constants import BASE_DIR, IS_FROZEN
# --------------------------------------------------------------------- # ---------------------------------------------------------------------
@ -32,6 +35,16 @@ def get_build_info():
return result return result
def get_build_git_info():
"""Get the git details for the current build."""
if IS_FROZEN:
build_info = get_build_info()
if build_info:
return build_info[ "git_info" ]
elif app.config.get( "IS_CONTAINER" ):
return os.environ.get( "BUILD_GIT_INFO" )
return None
# --------------------------------------------------------------------- # ---------------------------------------------------------------------
def catch_exceptions( caption="EXCEPTION", retval=None ): def catch_exceptions( caption="EXCEPTION", retval=None ):
@ -67,3 +80,14 @@ def show_msg_store( msg_store ):
for msg_type in ("error","warning"): for msg_type in ("error","warning"):
for msg in msg_store.get_msgs( msg_type ): for msg in msg_store.get_msgs( msg_type ):
MainWindow.showErrorMsg( msg ) MainWindow.showErrorMsg( msg )
# ---------------------------------------------------------------------
def launch_file( fname ):
"""Launch the specified file."""
if platform.system() == "Windows":
os.startfile( fname ) #pylint: disable=no-member
elif platform.system() == "Darwin":
subprocess.call( ("open", fname) )
else:
subprocess.call( ("xdg-open", fname) )

@ -38,12 +38,14 @@ class WebChannelHandler:
def on_new_scenario( self ): def on_new_scenario( self ):
"""Called when the scenario is reset.""" """Called when the scenario is reset."""
self.scenario_file_dialog.curr_fname = None self.scenario_file_dialog.curr_fname = None
self.updated_vsav_file_dialog.curr_fname = None
def load_scenario( self ): def load_scenario( self ):
"""Called when the user wants to load a scenario.""" """Called when the user wants to load a scenario."""
data = self.scenario_file_dialog.load_file( False ) data = self.scenario_file_dialog.load_file( False )
if data is None: if data is None:
return None, None return None, None
self.updated_vsav_file_dialog.curr_fname = None
return self.scenario_file_dialog.curr_fname, data return self.scenario_file_dialog.curr_fname, data
def save_scenario( self, fname, data ): def save_scenario( self, fname, data ):
@ -68,7 +70,7 @@ class WebChannelHandler:
title += " (*)" title += " (*)"
self.parent.setWindowTitle( title ) self.parent.setWindowTitle( title )
def on_snippet_image( self, img_data ): #pylint: disable=no-self-use def on_snippet_image( self, img_data ):
"""Called when a snippet image has been generated.""" """Called when a snippet image has been generated."""
# NOTE: We could maybe add an HTML object to the clipboard as well, but having two formats on the clipboard # NOTE: We could maybe add an HTML object to the clipboard as well, but having two formats on the clipboard
# simultaneously might confuse some programs, causing problems for no real benefit :shrug: # simultaneously might confuse some programs, causing problems for no real benefit :shrug:

@ -5,7 +5,6 @@ import os
import signal import signal
import threading import threading
import time import time
import tempfile
import configparser import configparser
import logging import logging
import logging.config import logging.config
@ -17,7 +16,6 @@ import yaml
from vasl_templates.webapp.config.constants import BASE_DIR from vasl_templates.webapp.config.constants import BASE_DIR
shutdown_event = threading.Event() shutdown_event = threading.Event()
_LOCK_FNAME = os.path.join( tempfile.gettempdir(), "vasl-templates.lock" )
# --------------------------------------------------------------------- # ---------------------------------------------------------------------
@ -92,9 +90,7 @@ def _init_webapp():
elif dname: elif dname:
webapp_vo_notes._vo_notes_image_cache_dname = dname #pylint: disable=protected-access webapp_vo_notes._vo_notes_image_cache_dname = dname #pylint: disable=protected-access
else: else:
webapp_vo_notes._vo_notes_image_cache_dname = os.path.join( #pylint: disable=protected-access webapp_vo_notes._vo_notes_image_cache_dname = globvars.user_profile.vo_notes_image_cache_dname #pylint: disable=protected-access
tempfile.gettempdir(), "vasl-templates", "vo-notes-image-cache"
)
# load integration data from asl-rulebook2 # load integration data from asl-rulebook2
from vasl_templates.webapp.vo_notes import load_asl_rulebook2_vo_note_targets #pylint: disable=cyclic-import from vasl_templates.webapp.vo_notes import load_asl_rulebook2_vo_note_targets #pylint: disable=cyclic-import
@ -133,8 +129,8 @@ def _is_flask_child_process():
# --------------------------------------------------------------------- # ---------------------------------------------------------------------
def _on_sigint( signum, stack ): #pylint: disable=unused-argument def _on_sig( signum, stack ): #pylint: disable=unused-argument
"""Clean up after a SIGINT.""" """Clean up after a SIGINT/SIGTERM."""
# FUDGE! Since we added gRPC test control, we want to shutdown properly and clean things up (e.g. temp files # FUDGE! Since we added gRPC test control, we want to shutdown properly and clean things up (e.g. temp files
# created by the gRPC service), but the Flask reloader complicates what we have to do here horribly :-( # created by the gRPC service), but the Flask reloader complicates what we have to do here horribly :-(
@ -159,13 +155,13 @@ def _on_sigint( signum, stack ): #pylint: disable=unused-argument
shutdown_event.set() shutdown_event.set()
# call any registered cleanup handlers # call any registered cleanup handlers
from vasl_templates.webapp import globvars #pylint: disable=cyclic-import
for handler in globvars.cleanup_handlers: for handler in globvars.cleanup_handlers:
handler() handler()
lock_fname = globvars.user_profile.flask_lock_fname
if _is_flask_child_process(): if _is_flask_child_process():
# notify the parent process that we're done # notify the parent process that we're done
os.unlink( _LOCK_FNAME ) os.unlink( lock_fname )
else: else:
# we are the Flask parent process (so we wait for the child process to finish) or Flask reloading # we are the Flask parent process (so we wait for the child process to finish) or Flask reloading
# is disabled (and the wait below will end immediately, because the lock file was never created). # is disabled (and the wait below will end immediately, because the lock file was never created).
@ -176,7 +172,7 @@ def _on_sigint( signum, stack ): #pylint: disable=unused-argument
# NOTE: os.path.isfile() and .exists() both return True even after the log file has gone!?!? # NOTE: os.path.isfile() and .exists() both return True even after the log file has gone!?!?
# Is somebody caching something somewhere? :-/ # Is somebody caching something somewhere? :-/
try: try:
with open( _LOCK_FNAME, "rb" ): with open( lock_fname, "rb" ):
pass pass
except FileNotFoundError: except FileNotFoundError:
break break
@ -187,10 +183,6 @@ def _on_sigint( signum, stack ): #pylint: disable=unused-argument
# initialize Flask # initialize Flask
app = Flask( __name__ ) app = Flask( __name__ )
if _is_flask_child_process():
# we are the Flask child process - create a lock file
with open( _LOCK_FNAME, "wb" ):
pass
# set config defaults # set config defaults
# NOTE: These are defined here since they are used by both the back- and front-ends. # NOTE: These are defined here since they are used by both the back- and front-ends.
@ -201,19 +193,30 @@ app.config[ "ASA_GET_SCENARIO_URL" ] = "https://aslscenarioarchive.com/rest/scen
app.config[ "ASA_MAX_VASL_SETUP_SIZE" ] = 200 # nb: KB app.config[ "ASA_MAX_VASL_SETUP_SIZE" ] = 200 # nb: KB
app.config[ "ASA_MAX_SCREENSHOT_SIZE" ] = 200 # nb: KB app.config[ "ASA_MAX_SCREENSHOT_SIZE" ] = 200 # nb: KB
# initialize logging
_config_dir = os.path.join( BASE_DIR, "config" )
_fname = os.path.join( _config_dir, "logging.yaml" )
if os.path.isfile( _fname ):
with open( _fname, "r", encoding="utf-8" ) as fp:
try:
logging.config.dictConfig( yaml.safe_load( fp ) )
except Exception as _ex: #pylint: disable=broad-except
logging.error( "Can't load the logging config: %s", _ex )
else:
# stop Flask from logging every request :-/
logging.getLogger( "werkzeug" ).setLevel( logging.WARNING )
# load the application configuration # load the application configuration
config_dir = os.path.join( BASE_DIR, "config" ) _fname = os.path.join( _config_dir, "app.cfg" )
_fname = os.path.join( config_dir, "app.cfg" )
_load_config( _fname, "System" ) _load_config( _fname, "System" )
# load any site configuration # load any site configuration
_fname = os.path.join( config_dir, "site.cfg" ) _fname = os.path.join( _config_dir, "site.cfg" )
_load_config( _fname, "Site Config" ) _load_config( _fname, "Site Config" )
# load any debug configuration # load any debug configuration
_fname = os.path.join( config_dir, "debug.cfg" ) _fname = os.path.join( _config_dir, "debug.cfg" )
if os.path.isfile( _fname ) : load_debug_config( _fname )
load_debug_config( _fname )
# load any config from environment variables (e.g. set in the Docker container) # load any config from environment variables (e.g. set in the Docker container)
# NOTE: We could add these settings to the container's site.cfg, so that they are always defined, and things # NOTE: We could add these settings to the container's site.cfg, so that they are always defined, and things
@ -227,20 +230,20 @@ _set_config_from_env( "VASL_EXTNS_DIR" )
_set_config_from_env( "BOARDS_DIR" ) _set_config_from_env( "BOARDS_DIR" )
_set_config_from_env( "CHAPTER_H_NOTES_DIR" ) _set_config_from_env( "CHAPTER_H_NOTES_DIR" )
_set_config_from_env( "USER_FILES_DIR" ) _set_config_from_env( "USER_FILES_DIR" )
_set_config_from_env( "ASL_RULEBOOK2_BASE_URL" )
# NOTE: The Docker container also sets DEFAULT_TEMPLATE_PACK, but we read it directly from # NOTE: The Docker container also sets DEFAULT_TEMPLATE_PACK, but we read it directly from
# the environment variable, since it is not something that is stored in app.config. # the environment variable, since it is not something that is stored in app.config.
# initialize logging # initialize the user profile
_fname = os.path.join( config_dir, "logging.yaml" ) from vasl_templates.webapp.user_profile import UserProfile #pylint: disable=cyclic-import
if os.path.isfile( _fname ): from vasl_templates.webapp import globvars #pylint: disable=cyclic-import
with open( _fname, "r", encoding="utf-8" ) as fp: globvars.user_profile = UserProfile( app.config )
try:
logging.config.dictConfig( yaml.safe_load( fp ) ) # check if we are the Flask child process
except Exception as _ex: #pylint: disable=broad-except if _is_flask_child_process():
logging.error( "Can't load the logging config: %s", _ex ) # yup - create a lock file
else: with open( globvars.user_profile.flask_lock_fname, "wb" ):
# stop Flask from logging every request :-/ pass
logging.getLogger( "werkzeug" ).setLevel( logging.WARNING )
# load the application # load the application
import vasl_templates.webapp.main #pylint: disable=cyclic-import import vasl_templates.webapp.main #pylint: disable=cyclic-import
@ -255,7 +258,10 @@ import vasl_templates.webapp.downloads #pylint: disable=cyclic-import
import vasl_templates.webapp.lfa #pylint: disable=cyclic-import import vasl_templates.webapp.lfa #pylint: disable=cyclic-import
# install our signal handler (must be done in the main thread) # install our signal handler (must be done in the main thread)
signal.signal( signal.SIGINT, _on_sigint ) signal.signal( signal.SIGINT, _on_sig )
# NOTE: We must handle SIGTERM, so that "docker stop" and scaling down in Kubernetes
# work properly (otherwise it times out and we get SIGKILL'ed :-/).
signal.signal( signal.SIGTERM, _on_sig )
# register startup initialization # register startup initialization
app.before_request( _on_request ) app.before_request( _on_request )

@ -4,9 +4,10 @@ import sys
import os import os
APP_NAME = "VASL Templates" APP_NAME = "VASL Templates"
APP_VERSION = "v1.9" # nb: also update setup.py APP_VERSION = "v1.13" # nb: also update setup.py
APP_DESCRIPTION = "Generate HTML for use in VASL scenarios." APP_DESCRIPTION = "Generate HTML for use in VASL scenarios."
APP_HOME_URL = "https://vasl-templates.org" APP_HOME_URL = "https://vasl-templates.org"
APP_ISSUES_URL = "https://code.pacman-ghost.com/public/vasl-templates/issues"
if getattr( sys, "frozen", False ): if getattr( sys, "frozen", False ):
IS_FROZEN = True IS_FROZEN = True

@ -63,3 +63,7 @@ loggers:
level: "WARNING" level: "WARNING"
handlers: [ "console", "file" ] handlers: [ "console", "file" ]
propagate: 0 propagate: 0
user_profile:
level: "WARNING"
handlers: [ "console", "file" ]
propagate: 0

@ -2,7 +2,7 @@
; configure VASSAL and VASL ; configure VASSAL and VASL
VASSAL_DIR = ...configure the VASSAL installation directory... VASSAL_DIR = ...configure the VASSAL installation directory...
VASL_MOD = ...configure the VASL module (e.g. vasl-6.6.2.vmod)... VASL_MOD = ...configure the VASL module (e.g. vasl-6.6.7.vmod)...
VASL_EXTNS_DIR = ...configure the VASL extensions directory... VASL_EXTNS_DIR = ...configure the VASL extensions directory...
BOARDS_DIR = ...configure the VASL boards directory... BOARDS_DIR = ...configure the VASL boards directory...

@ -1,11 +1,12 @@
{ {
"_comment_": "This section maps theaters from those at the ASL Scenario Archive to ours.", "_comment_": "This section maps theaters from those at the ASL Scenario Archive to ours.",
"_comment2_": "CBI is handled in getEffectiveTheater().",
"theater-mappings": { "theater-mappings": {
"WTO": "ETO",
"MTO": "ETO", "MTO": "ETO",
"Normandy": "ETO", "Normandy": "ETO",
"KW": "Korea", "KW": "Korea"
"CBI": "PTO"
}, },
"_comment_": "This section maps player nationalities from those at the ASL Scenario Archive (must be lower-case) to our nationality ID's.", "_comment_": "This section maps player nationalities from those at the ASL Scenario Archive (must be lower-case) to our nationality ID's.",

@ -6,10 +6,12 @@ body {
p { margin-top: 5px ; margin-bottom: 0 ; } p { margin-top: 5px ; margin-bottom: 0 ; }
ul { margin: 0 ; padding: 0 0 0 10px ; } ul { margin: 0 ; padding: 0 0 0 10px ; }
ol { margin: 0 ; padding: 0 0 0 21px ; }
{%if CUSTOM_LIST_BULLETS%} {%if CUSTOM_LIST_BULLETS%}
ul { list-style-image: url("{{IMAGES_BASE_URL}}/bullet.png") ; } ul { list-style-image: url("{{IMAGES_BASE_URL}}/bullet.png") ; }
ul ul { list-style-image: url("{{IMAGES_BASE_URL}}/bullet2.png") ; } ul ul { list-style-image: url("{{IMAGES_BASE_URL}}/bullet2.png") ; }
ul ul ul { list-style-image: url("{{IMAGES_BASE_URL}}/bullet3.png") ; } ul ul ul { list-style-image: url("{{IMAGES_BASE_URL}}/bullet3.png") ; }
ol { list-style-image: none ; }
{%endif%} {%endif%}
td { margin: 0 ; padding: 0 ; } td { margin: 0 ; padding: 0 ; }

@ -0,0 +1,5 @@
<html> <!-- vasl-templates:id {{SNIPPET_ID}} -->
<img src="{{IMAGES_BASE_URL}}/compass/{{COMPASS}}.png">
</html>

@ -0,0 +1,41 @@
<html> <!-- vasl-templates:id {{SNIPPET_ID}} -->
<!-- vasl-templates:name Booby Traps -->
<!-- vasl-templates:description Data chart for Booby Traps. -->
<!-- player = {{PLAYER_DROPLIST:|Player}}
<!-- boards = {{BOARDS*:/8|Board(s)}} -->
<head>
<meta charset="utf-8">
<style>
.header {
background: {{PLAYER_COLORS[PLAYER_DROPLIST][0]}} ;
border-bottom: 1px solid {{PLAYER_COLORS[PLAYER_DROPLIST][2]}} ;
padding: 2px 5px ;
font-size: 105% ; font-weight: bold ;
}
.header .level { font-size: 90% ; font-style: italic ; }
{{CSS:common}}
</style>
</head>
<table>
<tr>
<td class="header">
<img src="{{PLAYER_FLAGS[PLAYER_DROPLIST]}}?prefh={{PLAYER_FLAG_SIZE_LARGE}}" width="{{PLAYER_FLAG_SIZE_LARGE}}" height="{{PLAYER_FLAG_SIZE_LARGE}}">&nbsp;Booby Traps <span class="level">(Level {{LEVEL:A::B::C/3|Level}})</span>
<tr>
<td style="padding:2px 5px;">
<b> {%if BOARDS%} Boards: {{BOARDS}} {%else%} Entire map {%endif%} </b>
<ul>
<li> Original TC
{% if LEVEL == "A" %} &ge; 11
{% elif LEVEL == "B" %} 11
{% elif LEVEL == "C" %} 12
{%else%} ??? {%endif%}
<li> Search Casualties
</ul>
</table>

@ -41,7 +41,7 @@
</table> --> </table> -->
<!-- vasl-templates:comment We don't include common.css because it's not necessary, and we want to keep the generated snippet short. --> <!-- vasl-templates:comment We don't include common.css because it's not necessary, and we want to keep the generated snippet short. -->
<!-- vasl-templates:comment {{CAPTION:(none)::Panzerfaust::Tank-Hunter Heroes|Caption}} --> <!-- vasl-templates:comment {{CAPTION:(none)::Panzerfaust::Tank-Hunter Heroes/11|Caption}} -->
{# NOTE: We specify the font size in pixels, rather than as a percentage, since this label should be added to a counter. #} {# NOTE: We specify the font size in pixels, rather than as a percentage, since this label should be added to a counter. #}
<div style="font-size:12px;font-weight:bold;"> <div style="font-size:12px;font-weight:bold;">
{%if CAPTION != "(none)"%}{{CAPTION}}:{%endif%} {{COUNT:/2|Number}} {%if CAPTION != "(none)"%}{{CAPTION}}:{%endif%} {{COUNT:/2|Number}}

@ -3,10 +3,10 @@
<!-- vasl-templates:name Grid --> <!-- vasl-templates:name Grid -->
<!-- vasl-templates:description Generates a grid. --> <!-- vasl-templates:description Generates a grid. -->
<!-- caption = {{CAPTION:/20|Grid caption}} --> <!-- caption = {{CAPTION*:/25|Caption}} -->
<!-- #cols = {{COLS:3/1|# columns}} ; #rows = {{ROWS:2/1|# rows}} --> <!-- #cols = {{COLS:3/1|# columns}} ; #rows = {{ROWS:2/1|# rows}} -->
<!-- cell size = {{CELL_WIDTH:180px/5|Cell width}} x {{CELL_HEIGHT:70px/5|Cell height}} --> <!-- cell size = {{CELL_WIDTH:180px/5|Cell width}} x {{CELL_HEIGHT:70px/5|Cell height}} -->
<!-- cell labels = {{CELL_LABELS:none::letters::numbers|Cell labels}} --> <!-- cell labels = {{CELL_LABELS:none::letters::numbers/6|Cell labels}} -->
<!-- color = {{PLAYER_COLOR_DROPLIST:|Border color}} ; border = {{BORDER_STYLE:solid::dotted::dashed::double::groove::ridge::inset::outset|Border style}} --> <!-- color = {{PLAYER_COLOR_DROPLIST:|Border color}} ; border = {{BORDER_STYLE:solid::dotted::dashed::double::groove::ridge::inset::outset|Border style}} -->
<head> <head>

@ -6,7 +6,7 @@
<head> <head>
<style> <style>
{{CSS:common}} {{CSS:common}}
/* {{CAVE_COMPLEX:Kakazu West::Kakazu Saddle::Kakazu Center::Kakazu Front::Kakazu Reverse::Kakazu East::Kakazu Village::(other)|Cave Complex}} */ /* {{CAVE_COMPLEX:Kakazu West::Kakazu Saddle::Kakazu Center::Kakazu Front::Kakazu Reverse::Kakazu East::Kakazu Village::(other)/10|Cave Complex}} */
.box { .box {
width: {{WIDTH:270px/5|Width}} ; width: {{WIDTH:270px/5|Width}} ;
height: {{HEIGHT:150px/5|Height}} ; height: {{HEIGHT:150px/5|Height}} ;

@ -1,7 +1,8 @@
<html> <!-- vasl-templates:id {{SNIPPET_ID}} --> <html> <!-- vasl-templates:id {{SNIPPET_ID}} -->
<!-- vasl-templates:name KGS Molotov Cocktails --> <!-- vasl-templates:name Kampfgruppe Scherer -->
<!-- vasl-templates:description Data chart for Molotov Cocktails in <i>Kampfgruppe Scherer</i>. --> <!-- vasl-templates:description Data charts for Grenade Bundles and Molotov Cocktails. -->
<!-- {{TYPE:Grenade Bundles::Molotov Cocktails/10|Data chart}} -->
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
@ -18,7 +19,34 @@
font-size: 105% ; font-weight: bold ; font-size: 105% ; font-weight: bold ;
"> ">
{# Some versions of Java require <img> tags to have the width and height specified!?! #} {# Some versions of Java require <img> tags to have the width and height specified!?! #}
{%if PLAYER_FLAGS["german"]%}<img src="{{PLAYER_FLAGS["german"]}}" {{PLAYER_FLAG_SIZE_LARGE}}>&nbsp;{%endif%}Molotov Cocktails {%if PLAYER_FLAGS["german"]%}<img src="{{PLAYER_FLAGS["german"]}}?prefh={{PLAYER_FLAG_SIZE_LARGE}}" width="{{PLAYER_FLAG_SIZE_LARGE}}" height="{{PLAYER_FLAG_SIZE_LARGE}}">&nbsp;{%endif%}{{TYPE}}
{% if TYPE == "Grenade Bundles" %}
<tr>
<td style="padding:3px 5px 0 5px;">
CC Attack -2 DRM
<tr>
<td style="padding:3px 5px 0 5px;">
<b>ATMM check</b>: dr &le; 3 (&#9651;)
<table style="margin-left:10px;">
<tr>
<td style="width:20px;"> +1
<td> HS/crew
<tr>
<td> +2
<td> SMC
<tr>
<td> +1
<td> CX
<tr>
<td> +1
<td> vs. non-armored vehicle
</table>
original 6 = pinned (CCV reduced by 1)
{% elif TYPE == "Molotov Cocktails" %}
<tr> <tr>
<td style="padding:3px 5px 0 5px;"> <td style="padding:3px 5px 0 5px;">
@ -51,6 +79,8 @@
<td style="padding:3px 5px 0 5px;"> <td style="padding:3px 5px 0 5px;">
<b>Kindling Attempt</b>: +2 DRM <b>Kindling Attempt</b>: +2 DRM
{%endif%}
</table> </table>
</html> </html>

@ -1,49 +0,0 @@
<html> <!-- vasl-templates:id {{SNIPPET_ID}} -->
<!-- vasl-templates:name KGS Grenade Bundles -->
<!-- vasl-templates:description Data chart for Grenade Bundles in <i>Kampfgruppe Scherer</i>. -->
<head>
<meta charset="utf-8">
<style> {{CSS:common}} </style>
</head>
<table>
<tr>
<td colspan="2" style="
background: {{PLAYER_COLORS["german"][0]}} ;
border-bottom: 1px solid {{PLAYER_COLORS["german"][2]}} ;
padding: 2px 5px ;
font-size: 105% ; font-weight: bold ;
">
{# Some versions of Java require <img> tags to have the width and height specified!?! #}
{%if PLAYER_FLAGS["german"]%}<img src="{{PLAYER_FLAGS["german"]}}" {{PLAYER_FLAG_SIZE_LARGE}}>&nbsp;{%endif%}Grenade Bundles
<tr>
<td style="padding:3px 5px 0 5px;">
CC Attack -2 DRM
<tr>
<td style="padding:3px 5px 0 5px;">
<b>ATMM check</b>: dr &le; 3 (&#9651;)
<table style="margin-left:10px;">
<tr>
<td style="width:20px;"> +1
<td> HS/crew
<tr>
<td> +2
<td> SMC
<tr>
<td> +1
<td> CX
<tr>
<td> +1
<td> vs. non-armored vehicle
</table>
original 6 = pinned (CCV reduced by 1)
</table>
</html>

@ -6,4 +6,4 @@
<!-- vasl-templates:comment We don't include common.css because it's not necessary, and we want to keep the generated snippet short. --> <!-- vasl-templates:comment We don't include common.css because it's not necessary, and we want to keep the generated snippet short. -->
<div style="font-size:110%"> <div style="font-size:110%">
<b>{{TYPE:Victory Points::Casualty VP::Exit VP|Type}}:</b> 0 <b>{{TYPE:Victory Points::Casualty VP::Exit VP/9|Type}}:</b> 0

@ -7,6 +7,7 @@
td { padding: 2px 5px ; } td { padding: 2px 5px ; }
li.comment { font-size: 96% ; font-style: italic ; color: #404040 ; } li.comment { font-size: 96% ; font-style: italic ; color: #404040 ; }
span.comment { font-size: 85% ; font-style: italic ; color: #404040 ; } span.comment { font-size: 85% ; font-style: italic ; color: #404040 ; }
.note-group { margin-top: 5px ; padding-top: 3px ; border-top: 1px solid #ccc ; }
</style> </style>
</head> </head>
@ -30,18 +31,20 @@ span.comment { font-size: 85% ; font-style: italic ; color: #404040 ; }
{%if NAT_CAPS.OBA_BLACK%} {%if NAT_CAPS.OBA_BLACK%}
<li> OBA: <span class="oba-black">{{NAT_CAPS.OBA_BLACK}}</span> <span class="oba-red">{{NAT_CAPS.OBA_RED}}</span> <li> OBA: <span class="oba-black">{{NAT_CAPS.OBA_BLACK}}</span> <span class="oba-red">{{NAT_CAPS.OBA_RED}}</span>
{%if NAT_CAPS.OBA_ACCESS%} <span class="oba-access">(access: {{NAT_CAPS.OBA_ACCESS}})</span> {%endif%} {%if NAT_CAPS.OBA_ACCESS%} <span class="oba-access">(access: {{NAT_CAPS.OBA_ACCESS}})</span> {%endif%}
</span>
{%if NAT_CAPS.OBA_COMMENTS%} {%if NAT_CAPS.OBA_COMMENTS%}
<ul class="oba-comments"> {%for cmt in NAT_CAPS.OBA_COMMENTS%} <li class="comment"> {{cmt}} {%endfor%} </ul> <ul class="oba-comments"> {%for cmt in NAT_CAPS.OBA_COMMENTS%} <li class="comment"> {{cmt}} {%endfor%} </ul>
{%endif%} {%endif%}
{%endif%} {%endif%}
</ul> </ul>
{%if NAT_CAPS.NOTES%} {% for group in NAT_CAPS.NOTE_GROUPS %}
<ul class="notes" style="margin-top:5px;"> {%for note in NAT_CAPS.NOTES%} <div class="note-group" {%if not group.CAPTION%}style="border-top:none;"{%endif%} >
<li> {{note}} {%if group.CAPTION %}<div class="caption"> {{group.CAPTION}} </div>{%endif%}
{%endfor%} </ul> {%if group.NOTES %}<ul> {%for note in group.NOTES%}
{%endif%} <li> {{note}} {%endfor%}
</ul> {%endif%}
</div>
{%endfor%}
{%else%} {%else%}

@ -8,7 +8,11 @@
"notes": [ "notes": [
"{? 10/1943- | Inherent PF | No Inherent PF | Inherent PF<sup>10/43+</sup> ?}", "{? 10/1943- | Inherent PF | No Inherent PF | Inherent PF<sup>10/43+</sup> ?}",
"{? 01/1944- | Inherent ATMM | No Inherent ATMM | Inherent ATMM<sup>44+</sup> ?}", "{? 01/1944- | Inherent ATMM | No Inherent ATMM | Inherent ATMM<sup>44+</sup> ?}",
"SS: <ul> <li> Disrupt &amp; RtPh Surrender NA <br> vs Russians <li> Massacre OK <li> {? 01/1944- | Squad Assault Fire | No Squad Assault Fire | Squad Assault Fire<sup>44+</sup> ?} </ul>" { "caption": "SS", "notes": [
"Disrupt &amp; RtPh Surrender NA <br> vs Russians",
"Massacre OK",
"{? 01/1944- | Squad Assault Fire | No Squad Assault Fire | Squad Assault Fire<sup>44+</sup> ?}"
] }
] ]
}, },
@ -34,7 +38,11 @@
"hob_drm": "0 DRM", "hob_drm": "0 DRM",
"grenades": "SMOKE", "grenades": "SMOKE",
"notes": [ "notes": [
"U.S.M.C.: <ul> <li> Disruption NA <li> 7-6-8 can Self-Deploy <li> Vehicle [EXC: LC] Crew: Army 1-2-6 </ul>" { "caption": "U.S.M.C.", "notes": [
"Disruption NA",
"7-6-8 can Self-Deploy",
"Vehicle [EXC: LC] Crew: Army 1-2-6"
] }
] ]
}, },
"kfw-american": { "kfw-american": {
@ -46,14 +54,30 @@
"hob_drm": [ "0 DRM", "+3 for Katusa; NA for TACP" ], "hob_drm": [ "0 DRM", "+3 for Katusa; NA for TACP" ],
"grenades": "SMOKE", "grenades": "SMOKE",
"notes": [ "notes": [
"Rangers: 6-6-8 <ul> <li> Self-Rally OK <li> Self-Deploy (1TC) &amp; Self-Recombine OK <li> Cowering NA <li> Commandos <li> No Non-Qualified Use penalty for RCL <li> No Captured Use penalty for Communist SW </ul>",
"Airborne: 6-6-7",
"{! 06/1950-08/1950 = Early KW U.S. Army rules: <ul> <li> Always Lax <li> Ammo Shortage <li> SW repair only on \"1\" <li> Radio/Phone Contact reduced by 1 <li> AFV Inherent Crews have Morale 7 <li> All motorized vehicles have Red MP </ul> !}", "{! 06/1950-08/1950 = Early KW U.S. Army rules: <ul> <li> Always Lax <li> Ammo Shortage <li> SW repair only on \"1\" <li> Radio/Phone Contact reduced by 1 <li> AFV Inherent Crews have Morale 7 <li> All motorized vehicles have Red MP </ul> !}",
"Katusa: As U.S. Army MMC <ul> <li> HoB +3 DRM <li> Leader Creation +1 drm {! 09/1950-10/1951 = <li> ELR 2 <li> Allied Troop penalties with U.S. leaders !} </ul>",
"Disruption NA", "Disruption NA",
"7-6-8 can Self-Deploy", "7-6-8 can Self-Deploy",
"Use 5-5-8 when: <ul> <li> U.S.M.C. ELR Replacement is in effect <li> U.S.M.C. MMC re-arms </ul>", "Use 5-5-8 when: <ul> <li> U.S.M.C. ELR Replacement is in effect <li> U.S.M.C. MMC re-arms </ul>",
"Tactical Air Control Party: <ul> <li> Inherent Radio (Contact = 9) <li> May set up HIP </ul>" { "caption": "Rangers (6-6-8)", "notes": [
"Self-Rally OK",
"Self-Deploy (1TC) &amp; Self-Recombine OK",
"Cowering NA",
"Commandos",
"No Non-Qualified Use penalty for RCL",
"No Captured Use penalty for Communist SW"
] },
{ "caption": "Airborne (6-6-7)", "allow_empty": true },
{ "caption": "Katusa", "notes": [
"As U.S. Army MMC",
"HoB +3 DRM",
"Leader Creation +1 drm",
"{! 09/1950-10/1951 = ELR 2 !}",
"{! 09/1950-10/1951 = Allied Troop penalties with U.S. leaders !}"
] },
{ "caption": "Tactical Air Control Party", "notes": [
"Inherent Radio (Contact = 9)",
"May set up HIP"
] }
] ]
}, },
@ -63,9 +87,18 @@
"hob_drm": "-1 DRM", "hob_drm": "-1 DRM",
"grenades": "{? 01/1944- | SMOKE | Smoke | SMOKE<sup>44+</sup> ?}", "grenades": "{? 01/1944- | SMOKE | Smoke | SMOKE<sup>44+</sup> ?}",
"notes": [ "notes": [
"Elite &amp; 1st Line: Cowering NA", { "caption": "Elite &amp; 1st Line", "notes": [
"ANZAC: Stealthy (unless Green)", "Cowering NA"
"Gurkha: <ul> <li> -1 CC DRM <li> Disrupt &amp; RtPh Surrender NA <li> Commando (unless Green) <li> Stealthy </ul>" ] },
{ "caption": "ANZAC", "notes": [
"Stealthy (unless Green)"
] },
{ "caption": "Gurkha", "notes": [
"-1 CC DRM",
"Disrupt &amp; RtPh Surrender NA",
"Commando (unless Green)",
"Stealthy"
] }
] ]
}, },
@ -73,10 +106,29 @@
"th_color": [ "Black", "AFV use Red TH#" ], "th_color": [ "Black", "AFV use Red TH#" ],
"oba": [ "6B", "2R" ], "oba_access": "&le; 1", "oba": [ "6B", "2R" ], "oba_access": "&le; 1",
"hob_drm": "+1 DRM", "hob_drm": "+1 DRM",
"grenades": "Smoke", "grenades": "Smoke"
"notes": [] },
"free-french": {
"th_color": "Black",
"oba": [ "8B", "2R" ], "oba_access": "&le; 2",
"hob_drm": "-1 DRM",
"grenades": "{? 01/1944- | SMOKE | Smoke | SMOKE<sup>44+</sup> ?}",
"notes": [
"{? 12/1943- | Assault Fire | No Assault Fire | Assault Fire<sup>12/43+</sup> ?}",
"{? 12/1943-05/1945 | Inherent Crews as British for Morale | | Inherent Crews as British for Morale<sup>12/43-5/45</sup> ?}",
{ "caption": "Elite &amp; 1st Line", "notes": [
"Cowering NA"
] },
{ "caption": "No Captured Use penalty", "notes": [
"U.S. MTR/BAZ",
"Vichy French SW",
"{? -11/1943 | British (f) vehicles/Guns/SW | | British (f) vehicles/Guns/SW <sup>-11/43</sup> ?}",
"{? 12/1943-05/1945 | British/French (a)/(f) SW | | British/French (a)/(f) SW<sup>12/43-5/45</sup> ?}"
] }
]
}, },
"italian": { "italian": {
"th_color": "Red", "th_color": "Red",
"oba": [ "7B", "3R" ], "oba_access": "&le; 1", "oba": [ "7B", "3R" ], "oba_access": "&le; 1",
@ -84,7 +136,13 @@
"grenades": "Smoke", "grenades": "Smoke",
"notes": [ "notes": [
"Escape NA", "Escape NA",
"1st Line &amp; Conscript: <ul> <li> Surrender on HoB Final DR &ge; 10 <li> Deploy NA <li> +1 CC Capture DRM NA <li> Always Lax <li> 1 PAATC </ul>" { "caption": "1st Line &amp; Conscript", "notes": [
"Surrender on HoB Final DR &ge; 10",
"Deploy NA",
"+1 CC Capture DRM NA",
"Always Lax",
"1 PAATC"
] }
] ]
}, },
@ -102,10 +160,14 @@
"Deploy (1TC) &amp; Recombine without Leader", "Deploy (1TC) &amp; Recombine without Leader",
"Self-Rally OK [EXC: Conscript]", "Self-Rally OK [EXC: Conscript]",
"Cowering NA [EXC: Conscript]", "Cowering NA [EXC: Conscript]",
"Elite &amp; 1st Line: <ul> <li> Always Stealthy <li> Use FT/DC as Elite <li> {? 07/1944- | Inherent PF | No Inherent PF | Inherent PF<sup>7/44+</sup> ?} </ul>",
"Ski-trained (don Skis = one MF)", "Ski-trained (don Skis = one MF)",
"Leader Creation NA", "Leader Creation NA",
"Captured Use penalties NA for Russian MG <br> [EXC: LMG in 1939; .50-cal]" "Captured Use penalties NA for Russian MG <br> [EXC: LMG in 1939; .50-cal]",
{ "caption": "Elite &amp; 1st Line", "notes": [
"Always Stealthy",
"Use FT/DC as Elite",
"{? 07/1944- | Inherent PF | No Inherent PF | Inherent PF<sup>7/44+</sup> ?}"
] }
] ]
}, },
@ -114,9 +176,13 @@
"oba": [ "6B", "3R"], "oba": [ "6B", "3R"],
"hob_drm": "0 DRM", "hob_drm": "0 DRM",
"notes": [ "notes": [
"1<sup>st</sup> Line Battle Hardening &rarr; Fanatic", "Extreme Winter effects NA",
"Allied Troops Captured Use penalties NA", { "caption": "1<sup>st</sup> Line", "notes": [
"Extreme Winter effects NA" "Battle Hardening &rarr; Fanatic"
] },
{ "caption": "Allied Troops", "notes": [
"Captured Use penalties NA"
] }
] ]
}, },
@ -127,9 +193,17 @@
"grenades": "Smoke", "grenades": "Smoke",
"notes": [ "notes": [
"Escape NA", "Escape NA",
"1st Line &amp; Conscript: <ul> <li> 1 PAATC <li> Surrender on HoB Final DR &ge; 10 </ul>", { "caption": "1st Line &amp; Conscript", "notes": [
"{? 03/1944- | Inherent PF in non-Crew MMC (Romanian<sup>3/44+</sup>; Hungarian<sup>6/44+</sup>) | No Inherent PF | Inherent PF (Romanian<sup>3/44+</sup>; Hungarian<sup>6/44+</sup>) ?}", "1 PAATC",
"{? 07/1943- | Inherent ATMM in Romanian non-Crew Elite &amp; 1st Line MMC (-2 CC DRM) | No Inherent ATMM | Inherent ATMM<sup>(7/43+)</sup> ?}" "Surrender on HoB Final DR &ge; 10"
] },
{ "caption": "Romanian, Hungarian", "notes": [
"{! -02/1944 = No Inherent PF | 03/1944-05/1944 = | 06/1944- = Inherent PF in non-Crew MMC | Inherent PF <span class='comment'>(Romanian<sup>3/44+</sup>, Hungarian<sup>6/44+</sup>)</span> !}"
] },
{ "caption": "Romanian", "notes": [
"{! 03/1944-05/1944 = Inherent PF in non-Crew MMC !}",
"{? 07/1943- | Inherent ATMM in non-Crew Elite <br> and 1st Line MMC (-2 CC DRM) | No Inherent ATMM | Inherent ATMM<sup>(7/43+)</sup> ?}"
] }
] ]
}, },
@ -140,7 +214,9 @@
"grenades": "Smoke", "grenades": "Smoke",
"notes": [ "notes": [
"+1 Broken Morale vs Italians", "+1 Broken Morale vs Italians",
"1st Line &amp; Green: 1 PAATC" { "caption": "1st Line &amp; Green", "notes": [
"1 PAATC"
] }
] ]
}, },
@ -151,19 +227,34 @@
"grenades": "SMOKE", "grenades": "SMOKE",
"notes": [ "notes": [
"SMC PTC/Pin/Break NA", "SMC PTC/Pin/Break NA",
"Leaders: <ul> <li> Replacement NA <li> Casualty MC &rarr; elimination <li> Morale/Rally/Berserk as Commissar </ul>",
"Tank-Hunter Heroes &amp; ATMM", "Tank-Hunter Heroes &amp; ATMM",
"Banzai Charge (always Lax)", "Banzai Charge (always Lax)",
"Elite &amp; 1st Line: Always Stealthy",
"Conscript: Always Lax",
"ATR/MMG/HMG Breakdown penalty", "ATR/MMG/HMG Breakdown penalty",
"Always NA: <ul> <li> PAATC <li> Escape <li> RtPh Surrender <li> Disruption <li> Encircled lower Morale <li> Leader Creation </ul>",
"LLMC &rarr; LLTC if unbroken", "LLMC &rarr; LLTC if unbroken",
"Massacre OK", "Massacre OK",
"-1 Interrogation DRM", "-1 Interrogation DRM",
"-2 Concealment drm", "-2 Concealment drm",
"Enemy +2 search drm", "Enemy +2 search drm",
"Hand-to-Hand CC &amp; Hara-Kiri" "Hand-to-Hand CC &amp; Hara-Kiri",
{ "caption": "Leaders", "notes": [
"Replacement NA",
"Casualty MC &rarr; elimination",
"Morale/Rally/Berserk as Commissar"
] },
{ "caption": "Elite &amp; 1st Line", "notes": [
"Always Stealthy"
] },
{ "caption": "Conscript", "notes": [
"Always Lax"
] },
{ "caption": "Always NA", "notes": [
"PAATC",
"Escape",
"RtPh Surrender",
"Disruption",
"Encircled lower Morale",
"Leader Creation"
] }
] ]
}, },
@ -180,9 +271,11 @@
"Deploy NA", "Deploy NA",
"Lax at Night", "Lax at Night",
"+1 Leader Creation drm", "+1 Leader Creation drm",
"1st Line &amp; Conscript: 1 PAATC",
"Human Wave", "Human Wave",
"Dare-Death Squads [EXC: 5-3-7]" "Dare-Death Squads [EXC: 5-3-7]",
{ "caption": "1st Line &amp; Conscript", "notes": [
"1 PAATC"
] }
] ]
}, },
@ -213,7 +306,8 @@
}, },
"kfw-rok": { "kfw-rok": {
"th_color": "{! -08/1950 = Red | 09/1950-04/1951 = Red (ROK) ; Black (KMC) | 05/1951- = Black | ??? !}", "_comment_": "Errata (ASLJ 13 p48): KMC: '9/50+ black' s.b. '8/50+ black",
"th_color": "{! -07/1950 = Red | 08/1950-04/1951 = Red (ROK) ; Black (KMC) | 05/1951- = Black | ??? !}",
"oba": [ "{! 06/1950- = 10B | ??? !}", "3R", "oba": [ "{! 06/1950- = 10B | ??? !}", "3R",
"{? 09/1950- | Plentiful Ammo included | Plentiful Ammo included (KMC) | Plentiful Ammo included (ROK: 9/50+) ?}", "{? 09/1950- | Plentiful Ammo included | Plentiful Ammo included (KMC) | Plentiful Ammo included (ROK: 9/50+) ?}",
"{! 06/1950-08/1950 = ROK: 6B/3R !}" "{! 06/1950-08/1950 = ROK: 6B/3R !}"
@ -222,8 +316,16 @@
"hob_drm": "+3/+4 DRM", "hob_drm": "+3/+4 DRM",
"grenades": "SMOKE", "grenades": "SMOKE",
"notes": [ "notes": [
"Republic of Korea (ROK): <ul> {! 06/1946-04/1951 = <li> Early KW ROK rules !} <li> 1st Line MMC Battle-Harden to Fanatic <li> 2nd Line &amp; Conscript MMC: <ul> <li> Always Lax <li> Deploy NA </ul> {? -10/1950 | <li> Human Bullets | | <li> Human Bullets (pre-11/50) ?} </ul>", { "caption": "Republic of Korea (ROK)", "notes": [
"Korean Marine Corps (KMC): <ul> {! 04/1949-07/1950 = <li> Japanese-Armed KMC | 08/1950- = <li> U.S.-Armed KMC !} {? -01/1951 | <li> SW B#/X#/ROF penalty | | <li> SW B#/X#/ROF penalty (pre-2/51) ?} </ul>" "{! 06/1946-04/1951 = Early KW ROK rules !}",
"1st Line MMC: <ul> <li> Battle-Harden to Fanatic </ul>",
"2nd Line &amp; Conscript MMC: <ul> <li> Always Lax <li> Deploy NA",
"{? -10/1950 | Human Bullets | | Human Bullets (pre-11/50) ?}"
] },
{ "caption": "Korean Marine Corps (KMC)", "notes": [
"{! 04/1949-07/1950 = Japanese-Armed KMC | 08/1950- = U.S.-Armed KMC !}",
"{? -01/1951 | SW B#/X#/ROF penalty | | SW B#/X#/ROF penalty (pre-2/51) ?}"
] }
] ]
}, },
@ -233,9 +335,18 @@
"hob_drm": "-1 DRM", "hob_drm": "-1 DRM",
"grenades": "SMOKE", "grenades": "SMOKE",
"notes": [ "notes": [
"2nd Line MMC: ELR Replacement &rarr; Disrupt", { "caption": "2nd Line MMC", "notes": [
"{? 01/1952- | Canadian squads have Assault Fire | | Canadian squads have Assault Fire<sup>1/52+</sup> ?}", "ELR Replacement &rarr; Disrupt"
"Royal Marines: <ul> <li> Commandos <li> No Non-Qualified Use penalty for RCL <li> No Captured Use penalty for Communist SW <li> Self-Deploy (1TC) &amp; Self-Recombine OK </ul>" ] },
{ "caption": "Canadian", "notes": [
"{? 01/1952- | Squads have Assault Fire | | Squads have Assault Fire<sup>1/52+</sup> ?}"
] },
{ "caption": "Royal Marines", "notes": [
"Commandos",
"No Non-Qualified Use penalty for RCL",
"No Captured Use penalty for Communist SW",
"Self-Deploy (1TC) &amp; Self-Recombine OK"
] }
] ]
}, },
@ -245,8 +356,12 @@
"hob_drm": [ "0 DRM", "+3 for Turkish" ], "hob_drm": [ "0 DRM", "+3 for Turkish" ],
"grenades": "SMOKE", "grenades": "SMOKE",
"notes": [ "notes": [
"2nd Line MMC: ELR Replacement &rarr; Disrupt [EXC: Turkish]", { "caption": "2nd Line MMC", "notes": [
"Bayonet Charge NTC NA for Ethiopian, French, Turkish leaders" "ELR Replacement &rarr; Disrupt [EXC: Turkish]"
] },
{ "caption": "Ethiopian, French, Turkish", "notes": [
"Bayonet Charge NTC NA for leaders"
] }
] ]
}, },
@ -256,11 +371,24 @@
"hob_drm": "+2 DRM", "hob_drm": "+2 DRM",
"grenades": null, "grenades": null,
"notes": [ "notes": [
"As Russian <ul> <li> Elite Personnel always Stealthy <li> Elite Squads may Deploy <li> Commissars <li> Massacre OK <li> Human Wave by SSR only </ul>",
"Suicide Heroes", "Suicide Heroes",
"Starshell restrictions", "Starshell restrictions",
"Assault Engineers: WP grenades", { "caption": "As Russian", "notes": [
"Communist Partisans: <ul> <li> Neither Elite nor Conscript/Green <li> Always Stealthy <li> Massacre OK <li> Disrupt &amp; RtPh Surrender NA </ul>" "Elite Personnel always Stealthy",
"Elite Squads may Deploy",
"Commissars",
"Massacre OK",
"Human Wave by SSR only"
] },
{ "caption": "Assault Engineers", "notes": [
"WP grenades"
] },
{ "caption": "Communist Partisans", "notes": [
"Neither Elite nor Conscript/Green",
"Always Stealthy",
"Massacre OK",
"Disrupt &amp; RtPh Surrender NA"
] }
] ]
}, },
@ -278,7 +406,6 @@
"Starshell restrictions", "Starshell restrictions",
"Armored Assault NA", "Armored Assault NA",
"Riders NA", "Riders NA",
"Assault Engineers: WP grenades",
"{! 10/1950-03/1951 = Early KW CPVA rules !}", "{! 10/1950-03/1951 = Early KW CPVA rules !}",
"Leaders &amp; Political Officers increase Morale <br> as if Commissar", "Leaders &amp; Political Officers increase Morale <br> as if Commissar",
"SW B#/X#/ROF penalty", "SW B#/X#/ROF penalty",
@ -292,7 +419,10 @@
"Infantry Overrun NTC NA", "Infantry Overrun NTC NA",
"Conceal if +2 Hindrance", "Conceal if +2 Hindrance",
"Concealment -1 drm", "Concealment -1 drm",
"Civilian Interrogation is always in effect" "Civilian Interrogation is always in effect",
{ "caption": "Assault Engineers", "notes": [
"WP grenades"
] }
] ]
}, },
@ -303,9 +433,13 @@
"grenades": null, "grenades": null,
"notes": [ "notes": [
"Dare-Death Squads (as if Chinese)", "Dare-Death Squads (as if Chinese)",
"Elite and 1st Line MMC: Always Stealthy",
"Deploy NA [EXC: A20.5 &amp; A21.22]; Recombine OK", "Deploy NA [EXC: A20.5 &amp; A21.22]; Recombine OK",
"Leaders: Morale/Berserk/Rally as Commissar" { "caption": "Elite and 1st Line MMC", "notes": [
"Always Stealthy"
] },
{ "caption": "Leaders", "notes": [
"Morale/Berserk/Rally as Commissar"
] }
] ]
}, },

@ -1,4 +1,4 @@
/* NOTE: This CSS is split out into a separate file so we can apply it when generating Chapter H notes as images. */ {# NOTE: This CSS is split out into a separate file so we can apply it when generating Chapter H notes as images. #}
img.piece { float: left ; margin-right: 0.5em ; } img.piece { float: left ; margin-right: 0.5em ; }
@ -22,3 +22,6 @@ img.piece { float: left ; margin-right: 0.5em ; }
table.layout td { padding: 0 5px ; } table.layout td { padding: 0 5px ; }
.content .rf { display: none ; } .content .rf { display: none ; }
{# FUDGE! VASSAL and modern browsers differ on how much left margin there should be :-/ The default setting works on VASSAL, but because I serve Chapter H as images (which get generated in a modern browser), we change things to suit that. Note that there are other places where unordered lists are used (e.g. multi-applicable notes), but since those are typically only shown in VASSAL, we let them use the VASSAL-preferred setting. #}
.content ul { margin-left: 5px ; }

@ -1,2 +1,2 @@
{# Some versions of Java require <img> tags to have the width and height specified!?! #} {# Some versions of Java require <img> tags to have the width and height specified!?! #}
{%if PLAYER_FLAG%}<img src="{{PLAYER_FLAG}}" {{PLAYER_FLAG_SIZE}}>&nbsp;{%endif%} {%if PLAYER_FLAG%}<img src="{{PLAYER_FLAG}}?prefh={{PLAYER_FLAG_SIZE}}" width="{{PLAYER_FLAG_SIZE}}" height="{{PLAYER_FLAG_SIZE}}">&nbsp;{%endif%}

@ -1,2 +1,2 @@
{# Some versions of Java require <img> tags to have the width and height specified!?! #} {# Some versions of Java require <img> tags to have the width and height specified!?! #}
{%if PLAYER_FLAG%}<img src="{{PLAYER_FLAG}}" {{PLAYER_FLAG_SIZE_LARGE}}>&nbsp;{%endif%} {%if PLAYER_FLAG%}<img src="{{PLAYER_FLAG}}?prefh={{PLAYER_FLAG_SIZE_LARGE}}" width="{{PLAYER_FLAG_SIZE_LARGE}}" height="{{PLAYER_FLAG_SIZE_LARGE}}">&nbsp;{%endif%}

@ -26,7 +26,7 @@ td.description { font-size: 90% ; font-style: italic ; color: #808080 ; }
<tr> <tr>
{# Some versions of Java require <img> tags to have the width and height specified!?! #} {# Some versions of Java require <img> tags to have the width and height specified!?! #}
<td class="flag"> {%if PLAYER_FLAG_1%} <img src="{{PLAYER_FLAG_1}}" {{PLAYER_FLAG_SIZE}}> {%endif%} <td class="flag"> {%if PLAYER_FLAG_1%} <img src="{{PLAYER_FLAG_1}}?prefh={{PLAYER_FLAG_SIZE}}" width="{{PLAYER_FLAG_SIZE}}" height="{{PLAYER_FLAG_SIZE}}"> {%endif%}
<td class="player"> {{PLAYER_1_NAME}}: <td class="player"> {{PLAYER_1_NAME}}:
<td class="val"> &nbsp; ELR: {{PLAYER_1_ELR}} <td class="val"> &nbsp; ELR: {{PLAYER_1_ELR}}
<td class="val"> &nbsp; SAN: {{PLAYER_1_SAN}} <td class="val"> &nbsp; SAN: {{PLAYER_1_SAN}}
@ -39,7 +39,7 @@ td.description { font-size: 90% ; font-style: italic ; color: #808080 ; }
<tr> <tr>
{# Some versions of Java require <img> tags to have the width and height specified!?! #} {# Some versions of Java require <img> tags to have the width and height specified!?! #}
<td class="flag"> {%if PLAYER_FLAG_2%} <img src="{{PLAYER_FLAG_2}}" {{PLAYER_FLAG_SIZE}}> {%endif%} <td class="flag"> {%if PLAYER_FLAG_2%} <img src="{{PLAYER_FLAG_2}}?prefh={{PLAYER_FLAG_SIZE}}" width="{{PLAYER_FLAG_SIZE}}" height="{{PLAYER_FLAG_SIZE}}"> {%endif%}
<td class="player"> {{PLAYER_2_NAME}}: <td class="player"> {{PLAYER_2_NAME}}:
<td class="val"> &nbsp; ELR: {{PLAYER_2_ELR}} <td class="val"> &nbsp; ELR: {{PLAYER_2_ELR}}
<td class="val"> &nbsp; SAN: {{PLAYER_2_SAN}} <td class="val"> &nbsp; SAN: {{PLAYER_2_SAN}}

@ -1,9 +1,11 @@
<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}}{%if BUILD_GIT_INFO%} ({{BUILD_GIT_INFO}}){%endif%}:
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%}
--> {%endif%} --> {%endif%}
{%if ASA_ID %}<!-- vasl-templates:ASA_ID {{ASA_ID}} -->{%endif%}
{%if ROAR_ID %}<!-- vasl-templates:ROAR_ID {{ROAR_ID}} -->{%endif%}
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">

@ -4,7 +4,7 @@
<meta charset="utf-8"> <meta charset="utf-8">
<style> <style>
{{CSS:common}} {{CSS:common}}
ul#ssr { padding: 0 5px 0 13px ; } ul#ssr { padding: 0 5px 0 15px ; }
ul#ssr li { margin-top: 2px ; } ul#ssr li { margin-top: 2px ; }
</style> </style>
</head> </head>

@ -0,0 +1,116 @@
<html> <!-- vasl-templates:id {{SNIPPET_ID}} -->
<head>
<meta charset="utf-8">
<style> {{CSS:common}} </style>
<style>
td {
width: 50px ; min-width: 50px ;
height: {%if TURN_TRACK_PREVIEW_MODE%} 50px {%else%} 43px {%endif%} ;
padding: 2px ;
border: 1px solid black ;
}
{% set RESET_TD = "min-width: unset ; height: unset ; padding: 0 ; border: none" %}
td.turn-no {
{{RESET_TD}} ; width: unset ;
text-align: center ; vertical-align: center ; font-size: 18px ; font-weight: bold ;
}
td.no-reinforce { {{RESET_TD}} ; width: 13px ; }
{# NOTE: We do the reinforcement flags as CSS backgrounds, since VASSAL is incredibly slow downloading normal images. #}
td.reinforce1 { {{RESET_TD}} ; width: 13px ; background: url("{{TURN_TRACK_FLAG_1}}") top left no-repeat ; vertical-align: top ; }
td.reinforce2 { {{RESET_TD}} ; width: 13px ; background: url("{{TURN_TRACK_FLAG_2}}") bottom right no-repeat ; vertical-align: bottom ; }
td.half-turn {
background: url("{{TURN_TRACK_HALF_TURN_IMAGE_URL}}") bottom right no-repeat ;
background-size: contain ; {# nb: doesn't work in VASSAL, the image file needs to be the correct size :-/ #}
}
{% if TURN_TRACK_PREVIEW_MODE %}
body { margin: 0 ; }
body ::selection {}
body ::moz-selection {}
body { user-select: none ; }
.reinforce1, .reinforce2 { opacity: 0 ; }
.flag-click { width: 13px ; height: 13px ; cursor: pointer ; }
.shading-click { cursor: pointer ; }
{%endif%}
</style>
</head>
{% if TURN_TRACK_PREVIEW_MODE %}
<script>
// notify the parent window of clicks
function onFlagClick( turnNo, playerNo ) {
window.parent.postMessage( {
type: "FlagClick",
turnNo: turnNo, uiPlayerNo: playerNo
}, "*" ) ;
}
function onShadingClick( turnNo ) {
window.parent.postMessage( {
type: "ShadingClick",
turnNo: turnNo
}, "*" ) ;
}
</script>
{%endif%}
<table class="turn-track">
{% for row in TURN_TRACK_SQUARES %}
<tr>
{% for turnSquare in row %}
<td id="turn-square-{{turnSquare[0]}}"
{%if turnSquare[0] == TURN_TRACK_HALF_TURN%} class="half-turn" {%endif%}
{% if turnSquare[3] %} style="background-color:{{turnSquare[3]}};" {%endif%}
>
<table style="width:100%;height:100%;"> <tr>
<td id="flag-{{turnSquare[0]}}_1" width="100%"
class = {% if turnSquare[1] %} "reinforce1" {%else%} "no-reinforce" {%endif%}
>
{% if TURN_TRACK_PREVIEW_MODE %}
<div class="flag-click"
onclick = "onFlagClick( {{turnSquare[0]}}, 1 )"
> </div>
{%endif%}
</td>
<td class="turn-no">
{% if TURN_TRACK_PREVIEW_MODE %}
<div class="shading-click"
onclick = "onShadingClick( {{turnSquare[0]}} )"
>
{{turnSquare[0]}}
</div>
{%else%}
{{turnSquare[0]}}
{%endif%}
</td>
<td id="flag-{{turnSquare[0]}}_2" width="100%"
class = {% if turnSquare[2] and turnSquare[0] != TURN_TRACK_HALF_TURN %} "reinforce2" {%else%} "no-reinforce" {%endif%}
>
{% if TURN_TRACK_PREVIEW_MODE and turnSquare[0] != TURN_TRACK_HALF_TURN %}
<div class="flag-click"
onclick = "onFlagClick( {{turnSquare[0]}}, 2 )"
> </div>
{%endif%}
</td>
</tr> </table>
</td>
{%endfor%}
</tr>
{%endfor%}
</table>
</html>

@ -5,7 +5,7 @@
"note_number": "1", "note_number": "1",
"notes": [ "A", "B", "C\u2020", "E" ], "notes": [ "A", "B", "C\u2020", "E" ],
"id": "fr/o:000", "id": "fr/o:000",
"gpid": 1636 "gpid": 12830
}, },
{ "name": "Mortier de 60 mle 35", { "name": "Mortier de 60 mle 35",
"type": "MTR", "type": "MTR",
@ -13,7 +13,7 @@
"note_number": "2\u2020", "note_number": "2\u2020",
"notes": [ "A", "B", "V" ], "notes": [ "A", "B", "V" ],
"id": "fr/o:001", "id": "fr/o:001",
"gpid": 1641 "gpid": 1633
}, },
{ "name": "Mortier de 81 mle 27/31", { "name": "Mortier de 81 mle 27/31",
"type": "MTR", "type": "MTR",
@ -22,7 +22,7 @@
"note_number": "3\u2020", "note_number": "3\u2020",
"notes": [ "A", "B", "D", "V" ], "notes": [ "A", "B", "D", "V" ],
"id": "fr/o:002", "id": "fr/o:002",
"gpid": 1667 "gpid": [ 1665, 1667 ]
}, },
{ "name": "Fusil Antichar Boys", { "name": "Fusil Antichar Boys",
"type": "ATR", "type": "ATR",

@ -0,0 +1,65 @@
{
"6996": {"is_small": true},
"485": {"is_small": true},
"850": {"is_small": true},
"849": {"is_small": true},
"12689": {"is_small": true},
"856": {"is_small": true},
"857": {"is_small": true},
"11336": {"is_small": true},
"858": {"is_small": true},
"11337": {"is_small": true},
"1149": {"is_small": true},
"1153": {"is_small": true},
"12687": {"is_small": true},
"3b5:7613": {"is_small": true},
"11359": {"is_small": true},
"3b5:11259": {"is_small": true},
"1632": {"is_small": true},
"1636": {"is_small": true},
"1641": {"is_small": true},
"1648": {"is_small": true},
"1982": {"is_small": true},
"1983": {"is_small": true},
"1984": {"is_small": true},
"1985": {"is_small": true},
"1986": {"is_small": true},
"1987": {"is_small": true},
"1988": {"is_small": true},
"2172": {"is_small": true},
"2173": {"is_small": true},
"2176": {"is_small": true},
"2179": {"is_small": true},
"11391": {"is_small": true},
"11392": {"is_small": true},
"11395": {"is_small": true},
"11396": {"is_small": true},
"11440": {"is_small": true},
"3b5:8401": {"is_small": true},
"3b5:8402": {"is_small": true},
"2465": {"is_small": true},
"2474": {"is_small": true},
"3252": {"is_small": true},
"3253": {"is_small": true},
"3263": {"is_small": true},
"3422": {"is_small": true},
"3428": {"is_small": true},
"6730": {"is_small": true},
"3605": {"is_small": true},
"3608": {"is_small": true},
"6763": {"is_small": true},
"3679": {"is_small": true},
"3680": {"is_small": true},
"3681": {"is_small": true},
"3682": {"is_small": true},
"3691": {"is_small": true},
"3692": {"is_small": true},
"3959": {"is_small": true},
"11558": {"is_small": true},
"11559": {"is_small": true},
"3b5:10150": {"is_small": true},
"3b5:10151": {"is_small": true},
"11600": {"is_small": true},
"11604": {"is_small": true},
"3b5:7871": {"is_small": true}
}

@ -0,0 +1,65 @@
{
"6996": {"is_small": true},
"485": {"is_small": true},
"850": {"is_small": true},
"849": {"is_small": true},
"12689": {"is_small": true},
"856": {"is_small": true},
"857": {"is_small": true},
"11336": {"is_small": true},
"858": {"is_small": true},
"11337": {"is_small": true},
"1149": {"is_small": true},
"1153": {"is_small": true},
"12687": {"is_small": true},
"3b5:7613": {"is_small": true},
"11359": {"is_small": true},
"3b5:11259": {"is_small": true},
"1632": {"is_small": true},
"1636": {"is_small": true},
"1641": {"is_small": true},
"1648": {"is_small": true},
"1982": {"is_small": true},
"1983": {"is_small": true},
"1984": {"is_small": true},
"1985": {"is_small": true},
"1986": {"is_small": true},
"1987": {"is_small": true},
"1988": {"is_small": true},
"2172": {"is_small": true},
"2173": {"is_small": true},
"2176": {"is_small": true},
"2179": {"is_small": true},
"11391": {"is_small": true},
"11392": {"is_small": true},
"11395": {"is_small": true},
"11396": {"is_small": true},
"11440": {"is_small": true},
"3b5:8401": {"is_small": true},
"3b5:8402": {"is_small": true},
"2465": {"is_small": true},
"2474": {"is_small": true},
"3252": {"is_small": true},
"3253": {"is_small": true},
"3263": {"is_small": true},
"3422": {"is_small": true},
"3428": {"is_small": true},
"6730": {"is_small": true},
"3605": {"is_small": true},
"3608": {"is_small": true},
"6763": {"is_small": true},
"3679": {"is_small": true},
"3680": {"is_small": true},
"3681": {"is_small": true},
"3682": {"is_small": true},
"3691": {"is_small": true},
"3692": {"is_small": true},
"3959": {"is_small": true},
"11558": {"is_small": true},
"11559": {"is_small": true},
"3b5:10150": {"is_small": true},
"3b5:10151": {"is_small": true},
"11600": {"is_small": true},
"11604": {"is_small": true},
"3b5:7871": {"is_small": true}
}

@ -0,0 +1,65 @@
{
"6996": {"is_small": true},
"485": {"is_small": true},
"850": {"is_small": true},
"849": {"is_small": true},
"12689": {"is_small": true},
"856": {"is_small": true},
"857": {"is_small": true},
"11336": {"is_small": true},
"858": {"is_small": true},
"11337": {"is_small": true},
"1149": {"is_small": true},
"1153": {"is_small": true},
"12687": {"is_small": true},
"3b5:7613": {"is_small": true},
"11359": {"is_small": true},
"3b5:11259": {"is_small": true},
"1632": {"is_small": true},
"1636": {"is_small": true},
"1641": {"is_small": true},
"1648": {"is_small": true},
"1982": {"is_small": true},
"1983": {"is_small": true},
"1984": {"is_small": true},
"1985": {"is_small": true},
"1986": {"is_small": true},
"1987": {"is_small": true},
"1988": {"is_small": true},
"2172": {"is_small": true},
"2173": {"is_small": true},
"2176": {"is_small": true},
"2179": {"is_small": true},
"11391": {"is_small": true},
"11392": {"is_small": true},
"11395": {"is_small": true},
"11396": {"is_small": true},
"11440": {"is_small": true},
"3b5:8401": {"is_small": true},
"3b5:8402": {"is_small": true},
"2465": {"is_small": true},
"2474": {"is_small": true},
"3252": {"is_small": true},
"3253": {"is_small": true},
"3263": {"is_small": true},
"3422": {"is_small": true},
"3428": {"is_small": true},
"6730": {"is_small": true},
"3605": {"is_small": true},
"3608": {"is_small": true},
"6763": {"is_small": true},
"3679": {"is_small": true},
"3680": {"is_small": true},
"3681": {"is_small": true},
"3682": {"is_small": true},
"3691": {"is_small": true},
"3692": {"is_small": true},
"3959": {"is_small": true},
"11558": {"is_small": true},
"11559": {"is_small": true},
"3b5:10150": {"is_small": true},
"3b5:10151": {"is_small": true},
"11600": {"is_small": true},
"11604": {"is_small": true},
"3b5:7871": {"is_small": true}
}

@ -0,0 +1,65 @@
{
"6996": {"is_small": true},
"485": {"is_small": true},
"850": {"is_small": true},
"849": {"is_small": true},
"12689": {"is_small": true},
"856": {"is_small": true},
"857": {"is_small": true},
"11336": {"is_small": true},
"858": {"is_small": true},
"11337": {"is_small": true},
"1149": {"is_small": true},
"1153": {"is_small": true},
"12687": {"is_small": true},
"3b5:7613": {"is_small": true},
"11359": {"is_small": true},
"3b5:11259": {"is_small": true},
"1632": {"is_small": true},
"1636": {"is_small": true},
"1641": {"is_small": true},
"1648": {"is_small": true},
"1982": {"is_small": true},
"1983": {"is_small": true},
"1984": {"is_small": true},
"1985": {"is_small": true},
"1986": {"is_small": true},
"1987": {"is_small": true},
"1988": {"is_small": true},
"2172": {"is_small": true},
"2173": {"is_small": true},
"2176": {"is_small": true},
"2179": {"is_small": true},
"11391": {"is_small": true},
"11392": {"is_small": true},
"11395": {"is_small": true},
"11396": {"is_small": true},
"11440": {"is_small": true},
"3b5:8401": {"is_small": true},
"3b5:8402": {"is_small": true},
"2465": {"is_small": true},
"2474": {"is_small": true},
"3252": {"is_small": true},
"3253": {"is_small": true},
"3263": {"is_small": true},
"3422": {"is_small": true},
"3428": {"is_small": true},
"6730": {"is_small": true},
"3605": {"is_small": true},
"3608": {"is_small": true},
"6763": {"is_small": true},
"3679": {"is_small": true},
"3680": {"is_small": true},
"3681": {"is_small": true},
"3682": {"is_small": true},
"3691": {"is_small": true},
"3692": {"is_small": true},
"3959": {"is_small": true},
"11558": {"is_small": true},
"11559": {"is_small": true},
"3b5:10150": {"is_small": true},
"3b5:10151": {"is_small": true},
"11600": {"is_small": true},
"11604": {"is_small": true},
"3b5:7871": {"is_small": true}
}

@ -0,0 +1,65 @@
{
"6996": {"is_small": true},
"485": {"is_small": true},
"850": {"is_small": true},
"849": {"is_small": true},
"12689": {"is_small": true},
"856": {"is_small": true},
"857": {"is_small": true},
"11336": {"is_small": true},
"858": {"is_small": true},
"11337": {"is_small": true},
"1149": {"is_small": true},
"1153": {"is_small": true},
"12687": {"is_small": true},
"3b5:7613": {"is_small": true},
"11359": {"is_small": true},
"3b5:11259": {"is_small": true},
"1632": {"is_small": true},
"1636": {"is_small": true},
"1641": {"is_small": true},
"1648": {"is_small": true},
"1982": {"is_small": true},
"1983": {"is_small": true},
"1984": {"is_small": true},
"1985": {"is_small": true},
"1986": {"is_small": true},
"1987": {"is_small": true},
"1988": {"is_small": true},
"2172": {"is_small": true},
"2173": {"is_small": true},
"2176": {"is_small": true},
"2179": {"is_small": true},
"11391": {"is_small": true},
"11392": {"is_small": true},
"11395": {"is_small": true},
"11396": {"is_small": true},
"11440": {"is_small": true},
"3b5:8401": {"is_small": true},
"3b5:8402": {"is_small": true},
"2465": {"is_small": true},
"2474": {"is_small": true},
"3252": {"is_small": true},
"3253": {"is_small": true},
"3263": {"is_small": true},
"3422": {"is_small": true},
"3428": {"is_small": true},
"6730": {"is_small": true},
"3605": {"is_small": true},
"3608": {"is_small": true},
"6763": {"is_small": true},
"3679": {"is_small": true},
"3680": {"is_small": true},
"3681": {"is_small": true},
"3682": {"is_small": true},
"3691": {"is_small": true},
"3692": {"is_small": true},
"3959": {"is_small": true},
"11558": {"is_small": true},
"11559": {"is_small": true},
"3b5:10150": {"is_small": true},
"3b5:10151": {"is_small": true},
"11600": {"is_small": true},
"11604": {"is_small": true},
"3b5:7871": {"is_small": true}
}

@ -0,0 +1,49 @@
{
"1555": {
"name": "2pdr Portee",
"front_images": [ "br/vehicles/portee.gif", "br/vehicles/portee0.gif" ],
"back_images": null
},
"2212": {
"name": "76* INF FRC",
"front_images": [ "al/gun/alINF76.gif", "al/gun/alINF76u.gif" ],
"back_images": "al/gun/alINF76b.gif"
},
"2698": {
"name": "SPW 251/10",
"front_images": "ge/veh/spw25110.gif",
"back_images": [ "No_ATR.gif", "No_PSK.gif" ]
},
"7409": {
"name": "76 ItK/28 B(s)",
"front_images": "fi/gun/fiAA76L.png",
"back_images": [ "fi/gun/fiAA76L.png", "fi/gun/fiAA76LB.png" ]
},
"adf:1828": {
"name": "105 ART wz.29",
"front_images": "po/gun/poARTwz29-BFP.png",
"back_images": [ "po/gun/poARTwz29-BFP.png", "po/gun/poARTwz29-BFPb.png" ]
},
"adf:1829": {
"name": "120* ART wz09.31",
"front_images": "po/gun/poARTwz0931-BFP.png",
"back_images": [ "po/gun/poARTwz0931-BFP.png", "po/gun/poARTwz0931-BFPb.png" ]
},
"adf:1830": {
"name": "155 ART wz.17",
"front_images": "po/gun/poARTwz17-BFP.png",
"back_images": [ "po/gun/poARTwz17-BFP.png", "po/gun/poARTwz17-BFPb.png" ]
},
"3b5:3676": {
"name": "M19A1 MGMC",
"front_images": [ "us/veh/usM19A1MGMC(trailer)KFW.png", "us/veh/usM19A1MGMC(KFW).png" ],
"back_images": null
}
}

@ -0,0 +1,65 @@
{
"6996": {"is_small": true},
"485": {"is_small": true},
"850": {"is_small": true},
"849": {"is_small": true},
"12689": {"is_small": true},
"856": {"is_small": true},
"857": {"is_small": true},
"11336": {"is_small": true},
"858": {"is_small": true},
"11337": {"is_small": true},
"1149": {"is_small": true},
"1153": {"is_small": true},
"12687": {"is_small": true},
"3b5:7613": {"is_small": true},
"11359": {"is_small": true},
"3b5:11259": {"is_small": true},
"1632": {"is_small": true},
"1636": {"is_small": true},
"1641": {"is_small": true},
"1648": {"is_small": true},
"1982": {"is_small": true},
"1983": {"is_small": true},
"1984": {"is_small": true},
"1985": {"is_small": true},
"1986": {"is_small": true},
"1987": {"is_small": true},
"1988": {"is_small": true},
"2172": {"is_small": true},
"2173": {"is_small": true},
"2176": {"is_small": true},
"2179": {"is_small": true},
"11391": {"is_small": true},
"11392": {"is_small": true},
"11395": {"is_small": true},
"11396": {"is_small": true},
"11440": {"is_small": true},
"3b5:8401": {"is_small": true},
"3b5:8402": {"is_small": true},
"2465": {"is_small": true},
"2474": {"is_small": true},
"3252": {"is_small": true},
"3253": {"is_small": true},
"3263": {"is_small": true},
"3422": {"is_small": true},
"3428": {"is_small": true},
"6730": {"is_small": true},
"3605": {"is_small": true},
"3608": {"is_small": true},
"6763": {"is_small": true},
"3679": {"is_small": true},
"3680": {"is_small": true},
"3681": {"is_small": true},
"3682": {"is_small": true},
"3691": {"is_small": true},
"3692": {"is_small": true},
"3959": {"is_small": true},
"11558": {"is_small": true},
"11559": {"is_small": true},
"3b5:10150": {"is_small": true},
"3b5:10151": {"is_small": true},
"11600": {"is_small": true},
"11604": {"is_small": true},
"3b5:7871": {"is_small": true}
}

@ -0,0 +1,147 @@
{
"2474": {
"expected": {
"name": "Goliath",
"front_images": [ "ge/gegol.gif", "ge/gegolb.gif" ],
"back_images": null
},
"updated": {
"front_images": "ge/gegol.gif"
}
},
"1555": {
"expected": {
"name": "2pdr Portee",
"front_images": "br/vehicles/portee.gif",
"back_images": [ "br/vehicles/portee.gif", "br/vehicles/portee0.gif" ]
},
"updated": {
"front_images": [ "br/vehicles/portee.gif", "br/vehicles/portee0.gif" ],
"back_images": null
}
},
"3463": {
"expected": {
"name": "75L AA 75/46",
"front_images": [ "it/gun/itAA7546.gif", "it/gun/itAA7546b.gif" ],
"back_images": [ "it/gun/itAA7546b.gif", "it/gun/itAA7546lb.gif" ]
},
"updated": {
"front_images": "it/gun/itAA7546.gif",
"back_images": "it/gun/itAA7546b.gif"
}
},
"3776": {
"expected": {
"name": "37* INF Skoda IG",
"front_images": [ "ax/gun/buIN37s.gif", "ax/gun/buIN37s2.gif" ],
"back_images": "ax/gun/buIN37sb.gif"
},
"updated": {
"front_images": "ax/gun/buIN37s.gif"
}
},
"3777": {
"expected": {
"name": "70* INF Skoda IG",
"front_images": [ "ax/gun/buIN37s.gif", "ax/gun/buIN37s2.gif" ],
"back_images": "ax/gun/buIN37sb.gif"
},
"updated": {
"front_images": "ax/gun/buIN37s2.gif"
}
},
"6802": {
"expected": {
"name": "20L (4) AA",
"front_images": [ "fi/gun/fi20L4 _2.png", "fi/gun/fi20L4 _2 LIM.png" ],
"back_images": null
},
"updated": {
"front_images": "fi/gun/fi20L4 _2.png"
}
},
"6803": {
"expected": {
"name": "20L VKT (12) AA",
"front_images": [ "fi/gun/fi20L12.png", "fi/gun/fi20L12L.png" ],
"back_images": null
},
"updated": {
"front_images": "fi/gun/fi20L12.png"
}
},
"6804": {
"expected": {
"name": "40L Bofors AA (s)",
"front_images": [ "fi/gun/fi40L.png", "fi/gun/fi40LL.png" ],
"back_images": null
},
"updated": {
"front_images": "fi/gun/fi40L.png"
}
},
"adf:1824": {
"expected": {
"name": "37L AT PTP obr. 30",
"front_images": "ru/gun/ruAT37L.gif",
"back_images": "ru/gun/ruAT37Lb.gif"
},
"updated": {
"front_images": "ru/gun/ru37LPTPobr30.png"
}
},
"adf:1822": {
"expected": {
"name": "37* INF PP obr. 15R",
"front_images": "ru/gun/ruINF37s.gif",
"back_images": "ru/gun/ruINF37sb.gif"
},
"updated": {
"front_images": "ru/gun/ru37PPobr15R.png"
}
},
"adf:1823": {
"expected": {
"name": "76* INF PP obr. 27",
"front_images": "ru/gun/ruINF76s.gif",
"back_images": "ru/gun/ruINF76sb.gif"
},
"updated": {
"front_images": "ru/gun/ru76PPobr27.png"
}
},
"3b5:10093": {
"expected": {
"name": "SL truck",
"front_images": [ "sh/SL3b(KFW).png", "sh/SL4b(KFW).png", "sh/SL5b(KFW).png", "sh/SL6b(KFW).png", "sh/SL1b(KFW).png", "sh/SL2b(KFW).png" ],
"back_images": [ "sh/SL3(KFW).png", "sh/SL4(KFW).png", "sh/SL5(KFW).png", "sh/SL6(KFW).png", "sh/SL1(KFW).png", "sh/SL2(KFW).png" ]
},
"updated": {
"front_images": "us/veh/usSearchlight(KFW).png",
"back_images": null
}
},
"08d:75": {
"expected": {
"name": "RCL 75*",
"front_images": "amrcl75-malf.png",
"back_images": "dm-75rcl.gif"
},
"updated": {
"front_images": "amrcl75.png"
}
}
}

@ -0,0 +1,61 @@
{
"1555": {
"name": "2pdr Portee",
"front_images": [ "br/vehicles/portee.gif", "br/vehicles/portee0.gif" ],
"back_images": null
},
"2212": {
"name": "76* INF FRC",
"front_images": [ "al/gun/alINF76.gif", "al/gun/alINF76u.gif" ],
"back_images": "al/gun/alINF76b.gif"
},
"2698": {
"name": "SPW 251/10",
"front_images": "ge/veh/spw25110.gif",
"back_images": [ "No_ATR.gif", "No_PSK.gif" ]
},
"6765": {
"name": "81* MTR Krh/32",
"front_images": "fi/gun/fi81mmMTR.png",
"back_images": [ "fi/gun/fi81mmMTR.png", "fi/gun/fi81mmMTRB.png" ]
},
"6782": {
"name": "81* MTR Savu M42",
"front_images": [ "fi/gun/fi81mmMTR SavuB.png", "fi/gun/fi81mmMTR Savu.png" ],
"back_images": "fi/gun/fi81mmMTR SavuB.png"
},
"7409": {
"name": "76 ItK/28 B(s)",
"front_images": "fi/gun/fiAA76L.png",
"back_images": [ "fi/gun/fiAA76L.png", "fi/gun/fiAA76LB.png" ]
},
"adf:1828": {
"name": "105 ART wz.29",
"front_images": "po/gun/poARTwz29-BFP.png",
"back_images": [ "po/gun/poARTwz29-BFP.png", "po/gun/poARTwz29-BFPb.png" ]
},
"adf:1829": {
"name": "120* ART wz09.31",
"front_images": "po/gun/poARTwz0931-BFP.png",
"back_images": [ "po/gun/poARTwz0931-BFP.png", "po/gun/poARTwz0931-BFPb.png" ]
},
"adf:1830": {
"name": "155 ART wz.17",
"front_images": "po/gun/poARTwz17-BFP.png",
"back_images": [ "po/gun/poARTwz17-BFP.png", "po/gun/poARTwz17-BFPb.png" ]
},
"3b5:3676": {
"name": "M19A1 MGMC",
"front_images": [ "us/veh/usM19A1MGMC(trailer)KFW.png", "us/veh/usM19A1MGMC(KFW).png" ],
"back_images": null
}
}

@ -0,0 +1,71 @@
{
"6996": {"is_small": true},
"485": {"is_small": true},
"850": {"is_small": true},
"849": {"is_small": true},
"12689": {"is_small": true},
"856": {"is_small": true},
"857": {"is_small": true},
"11336": {"is_small": true},
"858": {"is_small": true},
"11337": {"is_small": true},
"1149": {"is_small": true},
"1153": {"is_small": true},
"12687": {"is_small": true},
"3b5:7613": {"is_small": true},
"11359": {"is_small": true},
"3b5:11259": {"is_small": true},
"1632": {"is_small": true},
"1633": {"is_small": true},
"1648": {"is_small": true},
"12830": {"is_small": true},
"1982": {"is_small": true},
"1983": {"is_small": true},
"1984": {"is_small": true},
"1985": {"is_small": true},
"1986": {"is_small": true},
"1987": {"is_small": true},
"1988": {"is_small": true},
"2172": {"is_small": true},
"2173": {"is_small": true},
"2176": {"is_small": true},
"2179": {"is_small": true},
"11391": {"is_small": true},
"11392": {"is_small": true},
"11395": {"is_small": true},
"11396": {"is_small": true},
"11440": {"is_small": true},
"3b5:8401": {"is_small": true},
"3b5:8402": {"is_small": true},
"2465": {"is_small": true},
"2474": {"is_small": true},
"3252": {"is_small": true},
"3253": {"is_small": true},
"3263": {"is_small": true},
"3422": {"is_small": true},
"3428": {"is_small": true},
"6730": {"is_small": true},
"3605": {"is_small": true},
"3608": {"is_small": true},
"6763": {"is_small": true},
"3679": {"is_small": true},
"3680": {"is_small": true},
"3681": {"is_small": true},
"3682": {"is_small": true},
"3691": {"is_small": true},
"3692": {"is_small": true},
"3959": {"is_small": true},
"11558": {"is_small": true},
"11559": {"is_small": true},
"3b5:10150": {"is_small": true},
"3b5:10151": {"is_small": true},
"11600": {"is_small": true},
"11604": {"is_small": true},
"3b5:7871": {"is_small": true},
"adf:1948": {"is_small": true},
"adf:75": {"is_small": true},
"adf:77": {"is_small": true},
"adf:76": {"is_small": true},
"adf:1407": {"is_small": true},
"08d:75": {"is_small": true}
}

@ -0,0 +1,147 @@
{
"2474": {
"expected": {
"name": "Goliath",
"front_images": [ "ge/gegol.gif", "ge/gegolb.gif" ],
"back_images": null
},
"updated": {
"front_images": "ge/gegol.gif"
}
},
"1555": {
"expected": {
"name": "2pdr Portee",
"front_images": "br/vehicles/portee.gif",
"back_images": [ "br/vehicles/portee.gif", "br/vehicles/portee0.gif" ]
},
"updated": {
"front_images": [ "br/vehicles/portee.gif", "br/vehicles/portee0.gif" ],
"back_images": null
}
},
"3463": {
"expected": {
"name": "75L AA 75/46",
"front_images": [ "it/gun/itAA7546.gif", "it/gun/itAA7546b.gif" ],
"back_images": [ "it/gun/itAA7546b.gif", "it/gun/itAA7546lb.gif" ]
},
"updated": {
"front_images": "it/gun/itAA7546.gif",
"back_images": "it/gun/itAA7546b.gif"
}
},
"3776": {
"expected": {
"name": "37* INF Skoda IG",
"front_images": [ "ax/gun/buIN37s.gif", "ax/gun/buIN37s2.gif" ],
"back_images": "ax/gun/buIN37sb.gif"
},
"updated": {
"front_images": "ax/gun/buIN37s.gif"
}
},
"3777": {
"expected": {
"name": "70* INF Skoda IG",
"front_images": [ "ax/gun/buIN37s.gif", "ax/gun/buIN37s2.gif" ],
"back_images": "ax/gun/buIN37sb.gif"
},
"updated": {
"front_images": "ax/gun/buIN37s2.gif"
}
},
"6802": {
"expected": {
"name": "20L (4) AA",
"front_images": [ "fi/gun/fi20L4 _2.png", "fi/gun/fi20L4 _2 LIM.png" ],
"back_images": null
},
"updated": {
"front_images": "fi/gun/fi20L4 _2.png"
}
},
"6803": {
"expected": {
"name": "20L VKT (12) AA",
"front_images": [ "fi/gun/fi20L12.png", "fi/gun/fi20L12L.png" ],
"back_images": null
},
"updated": {
"front_images": "fi/gun/fi20L12.png"
}
},
"6804": {
"expected": {
"name": "40L Bofors AA (s)",
"front_images": [ "fi/gun/fi40L.png", "fi/gun/fi40LL.png" ],
"back_images": null
},
"updated": {
"front_images": "fi/gun/fi40L.png"
}
},
"adf:1824": {
"expected": {
"name": "37L AT PTP obr. 30",
"front_images": "ru/gun/ruAT37L.gif",
"back_images": "ru/gun/ruAT37Lb.gif"
},
"updated": {
"front_images": "ru/gun/ru37LPTPobr30.png"
}
},
"adf:1822": {
"expected": {
"name": "37* INF PP obr. 15R",
"front_images": "ru/gun/ruINF37s.gif",
"back_images": "ru/gun/ruINF37sb.gif"
},
"updated": {
"front_images": "ru/gun/ru37PPobr15R.png"
}
},
"adf:1823": {
"expected": {
"name": "76* INF PP obr. 27",
"front_images": "ru/gun/ruINF76s.gif",
"back_images": "ru/gun/ruINF76sb.gif"
},
"updated": {
"front_images": "ru/gun/ru76PPobr27.png"
}
},
"3b5:10093": {
"expected": {
"name": "SL truck",
"front_images": [ "sh/SL3b(KFW).png", "sh/SL4b(KFW).png", "sh/SL5b(KFW).png", "sh/SL6b(KFW).png", "sh/SL1b(KFW).png", "sh/SL2b(KFW).png" ],
"back_images": [ "sh/SL3(KFW).png", "sh/SL4(KFW).png", "sh/SL5(KFW).png", "sh/SL6(KFW).png", "sh/SL1(KFW).png", "sh/SL2(KFW).png" ]
},
"updated": {
"front_images": "us/veh/usSearchlight(KFW).png",
"back_images": null
}
},
"08d:75": {
"expected": {
"name": "RCL 75*",
"front_images": "amrcl75-malf.png",
"back_images": "dm-75rcl.gif"
},
"updated": {
"front_images": "amrcl75.png"
}
}
}

@ -0,0 +1,72 @@
{
"1555": {
"name": "2pdr Portee",
"front_images": [ "br/vehicles/portee.gif", "br/vehicles/portee0.gif" ],
"back_images": null
},
"2212": {
"name": "76* INF FRC",
"front_images": [ "al/gun/alINF76.gif", "al/gun/alINF76u.gif" ],
"back_images": "al/gun/alINF76b.gif"
},
"2698": {
"name": "SPW 251/10",
"front_images": "ge/veh/spw25110.gif",
"back_images": [ "No_ATR.gif", "No_PSK.gif" ]
},
"6765": {
"name": "81* MTR Krh/32",
"front_images": "fi/gun/fi81mmMTR.png",
"back_images": [ "fi/gun/fi81mmMTR.png", "fi/gun/fi81mmMTRB.png" ]
},
"6782": {
"name": "81* MTR Savu M42",
"front_images": [ "fi/gun/fi81mmMTR SavuB.png", "fi/gun/fi81mmMTR Savu.png" ],
"back_images": "fi/gun/fi81mmMTR SavuB.png"
},
"6797": {
"name": "20L (4) AA (g)",
"front_images": [ "fi/gun/fi20L4.png", "fi/gun/fi20L4L.png" ]
},
"6801": {
"name": "20L (6) AA (g)",
"front_images": "fi/gun/fi20L6.png",
"back_images": [ "fi/gun/fi20L6.png", "fi/gun/fi20L6L.png" ]
},
"7409": {
"name": "76 ItK/28 B(s)",
"front_images": "fi/gun/fiAA76L.png",
"back_images": [ "fi/gun/fiAA76L.png", "fi/gun/fiAA76LB.png" ]
},
"adf:1828": {
"name": "105 ART wz.29",
"front_images": "po/gun/poARTwz29-BFP.png",
"back_images": [ "po/gun/poARTwz29-BFP.png", "po/gun/poARTwz29-BFPb.png" ]
},
"adf:1829": {
"name": "120* ART wz09.31",
"front_images": "po/gun/poARTwz0931-BFP.png",
"back_images": [ "po/gun/poARTwz0931-BFP.png", "po/gun/poARTwz0931-BFPb.png" ]
},
"adf:1830": {
"name": "155 ART wz.17",
"front_images": "po/gun/poARTwz17-BFP.png",
"back_images": [ "po/gun/poARTwz17-BFP.png", "po/gun/poARTwz17-BFPb.png" ]
},
"3b5:3676": {
"name": "M19A1 MGMC",
"front_images": [ "us/veh/usM19A1MGMC(trailer)KFW.png", "us/veh/usM19A1MGMC(KFW).png" ],
"back_images": null
}
}

@ -0,0 +1,71 @@
{
"6996": {"is_small": true},
"485": {"is_small": true},
"850": {"is_small": true},
"849": {"is_small": true},
"12689": {"is_small": true},
"856": {"is_small": true},
"857": {"is_small": true},
"11336": {"is_small": true},
"858": {"is_small": true},
"11337": {"is_small": true},
"1149": {"is_small": true},
"1153": {"is_small": true},
"12687": {"is_small": true},
"3b5:7613": {"is_small": true},
"11359": {"is_small": true},
"3b5:11259": {"is_small": true},
"1632": {"is_small": true},
"1633": {"is_small": true},
"1648": {"is_small": true},
"12830": {"is_small": true},
"1982": {"is_small": true},
"1983": {"is_small": true},
"1984": {"is_small": true},
"1985": {"is_small": true},
"1986": {"is_small": true},
"1987": {"is_small": true},
"1988": {"is_small": true},
"2172": {"is_small": true},
"2173": {"is_small": true},
"2176": {"is_small": true},
"2179": {"is_small": true},
"11391": {"is_small": true},
"11392": {"is_small": true},
"11395": {"is_small": true},
"11396": {"is_small": true},
"11440": {"is_small": true},
"3b5:8401": {"is_small": true},
"3b5:8402": {"is_small": true},
"2465": {"is_small": true},
"2474": {"is_small": true},
"3252": {"is_small": true},
"3253": {"is_small": true},
"3263": {"is_small": true},
"3422": {"is_small": true},
"3428": {"is_small": true},
"6730": {"is_small": true},
"3605": {"is_small": true},
"3608": {"is_small": true},
"6763": {"is_small": true},
"3679": {"is_small": true},
"3680": {"is_small": true},
"3681": {"is_small": true},
"3682": {"is_small": true},
"3691": {"is_small": true},
"3692": {"is_small": true},
"3959": {"is_small": true},
"11558": {"is_small": true},
"11559": {"is_small": true},
"3b5:10150": {"is_small": true},
"3b5:10151": {"is_small": true},
"11600": {"is_small": true},
"11604": {"is_small": true},
"3b5:7871": {"is_small": true},
"adf:1948": {"is_small": true},
"adf:75": {"is_small": true},
"adf:77": {"is_small": true},
"adf:76": {"is_small": true},
"adf:1407": {"is_small": true},
"08d:75": {"is_small": true}
}

@ -0,0 +1,147 @@
{
"2474": {
"expected": {
"name": "Goliath",
"front_images": [ "ge/gegol.gif", "ge/gegolb.gif" ],
"back_images": null
},
"updated": {
"front_images": "ge/gegol.gif"
}
},
"1555": {
"expected": {
"name": "2pdr Portee",
"front_images": "br/vehicles/portee.gif",
"back_images": [ "br/vehicles/portee.gif", "br/vehicles/portee0.gif" ]
},
"updated": {
"front_images": [ "br/vehicles/portee.gif", "br/vehicles/portee0.gif" ],
"back_images": null
}
},
"3463": {
"expected": {
"name": "75L AA 75/46",
"front_images": [ "it/gun/itAA7546.gif", "it/gun/itAA7546b.gif" ],
"back_images": [ "it/gun/itAA7546b.gif", "it/gun/itAA7546lb.gif" ]
},
"updated": {
"front_images": "it/gun/itAA7546.gif",
"back_images": "it/gun/itAA7546b.gif"
}
},
"3776": {
"expected": {
"name": "37* INF Skoda IG",
"front_images": [ "ax/gun/buIN37s.gif", "ax/gun/buIN37s2.gif" ],
"back_images": "ax/gun/buIN37sb.gif"
},
"updated": {
"front_images": "ax/gun/buIN37s.gif"
}
},
"3777": {
"expected": {
"name": "70* INF Skoda IG",
"front_images": [ "ax/gun/buIN37s.gif", "ax/gun/buIN37s2.gif" ],
"back_images": "ax/gun/buIN37sb.gif"
},
"updated": {
"front_images": "ax/gun/buIN37s2.gif"
}
},
"6802": {
"expected": {
"name": "20L (4) AA",
"front_images": [ "fi/gun/fi20L4 _2.png", "fi/gun/fi20L4 _2 LIM.png" ],
"back_images": null
},
"updated": {
"front_images": "fi/gun/fi20L4 _2.png"
}
},
"6803": {
"expected": {
"name": "20L VKT (12) AA",
"front_images": [ "fi/gun/fi20L12.png", "fi/gun/fi20L12L.png" ],
"back_images": null
},
"updated": {
"front_images": "fi/gun/fi20L12.png"
}
},
"6804": {
"expected": {
"name": "40L Bofors AA (s)",
"front_images": [ "fi/gun/fi40L.png", "fi/gun/fi40LL.png" ],
"back_images": null
},
"updated": {
"front_images": "fi/gun/fi40L.png"
}
},
"adf:1824": {
"expected": {
"name": "37L AT PTP obr. 30",
"front_images": "ru/gun/ruAT37L.gif",
"back_images": "ru/gun/ruAT37Lb.gif"
},
"updated": {
"front_images": "ru/gun/ru37LPTPobr30.png"
}
},
"adf:1822": {
"expected": {
"name": "37* INF PP obr. 15R",
"front_images": "ru/gun/ruINF37s.gif",
"back_images": "ru/gun/ruINF37sb.gif"
},
"updated": {
"front_images": "ru/gun/ru37PPobr15R.png"
}
},
"adf:1823": {
"expected": {
"name": "76* INF PP obr. 27",
"front_images": "ru/gun/ruINF76s.gif",
"back_images": "ru/gun/ruINF76sb.gif"
},
"updated": {
"front_images": "ru/gun/ru76PPobr27.png"
}
},
"3b5:10093": {
"expected": {
"name": "SL truck",
"front_images": [ "sh/SL3b(KFW).png", "sh/SL4b(KFW).png", "sh/SL5b(KFW).png", "sh/SL6b(KFW).png", "sh/SL1b(KFW).png", "sh/SL2b(KFW).png" ],
"back_images": [ "sh/SL3(KFW).png", "sh/SL4(KFW).png", "sh/SL5(KFW).png", "sh/SL6(KFW).png", "sh/SL1(KFW).png", "sh/SL2(KFW).png" ]
},
"updated": {
"front_images": "us/veh/usSearchlight(KFW).png",
"back_images": null
}
},
"08d:75": {
"expected": {
"name": "RCL 75*",
"front_images": "amrcl75-malf.png",
"back_images": "dm-75rcl.gif"
},
"updated": {
"front_images": "amrcl75.png"
}
}
}

@ -0,0 +1,72 @@
{
"1555": {
"name": "2pdr Portee",
"front_images": [ "br/vehicles/portee.gif", "br/vehicles/portee0.gif" ],
"back_images": null
},
"2212": {
"name": "76* INF FRC",
"front_images": [ "al/gun/alINF76.gif", "al/gun/alINF76u.gif" ],
"back_images": "al/gun/alINF76b.gif"
},
"2698": {
"name": "SPW 251/10",
"front_images": "ge/veh/spw25110.gif",
"back_images": [ "No_ATR.gif", "No_PSK.gif" ]
},
"6765": {
"name": "81* MTR Krh/32",
"front_images": "fi/gun/fi81mmMTR.png",
"back_images": [ "fi/gun/fi81mmMTR.png", "fi/gun/fi81mmMTRB.png" ]
},
"6782": {
"name": "81* MTR Savu M42",
"front_images": [ "fi/gun/fi81mmMTR SavuB.png", "fi/gun/fi81mmMTR Savu.png" ],
"back_images": "fi/gun/fi81mmMTR SavuB.png"
},
"6797": {
"name": "20L (4) AA (g)",
"front_images": [ "fi/gun/fi20L4.png", "fi/gun/fi20L4L.png" ]
},
"6801": {
"name": "20L (6) AA (g)",
"front_images": "fi/gun/fi20L6.png",
"back_images": [ "fi/gun/fi20L6.png", "fi/gun/fi20L6L.png" ]
},
"7409": {
"name": "76 ItK/28 B(s)",
"front_images": "fi/gun/fiAA76L.png",
"back_images": [ "fi/gun/fiAA76L.png", "fi/gun/fiAA76LB.png" ]
},
"adf:1828": {
"name": "105 ART wz.29",
"front_images": "po/gun/poARTwz29-BFP.png",
"back_images": [ "po/gun/poARTwz29-BFP.png", "po/gun/poARTwz29-BFPb.png" ]
},
"adf:1829": {
"name": "120* ART wz09.31",
"front_images": "po/gun/poARTwz0931-BFP.png",
"back_images": [ "po/gun/poARTwz0931-BFP.png", "po/gun/poARTwz0931-BFPb.png" ]
},
"adf:1830": {
"name": "155 ART wz.17",
"front_images": "po/gun/poARTwz17-BFP.png",
"back_images": [ "po/gun/poARTwz17-BFP.png", "po/gun/poARTwz17-BFPb.png" ]
},
"3b5:3676": {
"name": "M19A1 MGMC",
"front_images": [ "us/veh/usM19A1MGMC(trailer)KFW.png", "us/veh/usM19A1MGMC(KFW).png" ],
"back_images": null
}
}

@ -0,0 +1,71 @@
{
"6996": {"is_small": true},
"485": {"is_small": true},
"850": {"is_small": true},
"849": {"is_small": true},
"12689": {"is_small": true},
"856": {"is_small": true},
"857": {"is_small": true},
"11336": {"is_small": true},
"858": {"is_small": true},
"11337": {"is_small": true},
"1149": {"is_small": true},
"1153": {"is_small": true},
"12687": {"is_small": true},
"3b5:7613": {"is_small": true},
"11359": {"is_small": true},
"3b5:11259": {"is_small": true},
"1632": {"is_small": true},
"1633": {"is_small": true},
"1648": {"is_small": true},
"12830": {"is_small": true},
"1982": {"is_small": true},
"1983": {"is_small": true},
"1984": {"is_small": true},
"1985": {"is_small": true},
"1986": {"is_small": true},
"1987": {"is_small": true},
"1988": {"is_small": true},
"2172": {"is_small": true},
"2173": {"is_small": true},
"2176": {"is_small": true},
"2179": {"is_small": true},
"11391": {"is_small": true},
"11392": {"is_small": true},
"11395": {"is_small": true},
"11396": {"is_small": true},
"11440": {"is_small": true},
"3b5:8401": {"is_small": true},
"3b5:8402": {"is_small": true},
"2465": {"is_small": true},
"2474": {"is_small": true},
"3252": {"is_small": true},
"3253": {"is_small": true},
"3263": {"is_small": true},
"3422": {"is_small": true},
"3428": {"is_small": true},
"6730": {"is_small": true},
"3605": {"is_small": true},
"3608": {"is_small": true},
"6763": {"is_small": true},
"3679": {"is_small": true},
"3680": {"is_small": true},
"3681": {"is_small": true},
"3682": {"is_small": true},
"3691": {"is_small": true},
"3692": {"is_small": true},
"3959": {"is_small": true},
"11558": {"is_small": true},
"11559": {"is_small": true},
"3b5:10150": {"is_small": true},
"3b5:10151": {"is_small": true},
"11600": {"is_small": true},
"11604": {"is_small": true},
"3b5:7871": {"is_small": true},
"adf:1948": {"is_small": true},
"adf:75": {"is_small": true},
"adf:77": {"is_small": true},
"adf:76": {"is_small": true},
"adf:1407": {"is_small": true},
"08d:75": {"is_small": true}
}

@ -0,0 +1,147 @@
{
"2474": {
"expected": {
"name": "Goliath",
"front_images": [ "ge/gegol.gif", "ge/gegolb.gif" ],
"back_images": null
},
"updated": {
"front_images": "ge/gegol.gif"
}
},
"1555": {
"expected": {
"name": "2pdr Portee",
"front_images": "br/vehicles/portee.gif",
"back_images": [ "br/vehicles/portee.gif", "br/vehicles/portee0.gif" ]
},
"updated": {
"front_images": [ "br/vehicles/portee.gif", "br/vehicles/portee0.gif" ],
"back_images": null
}
},
"3463": {
"expected": {
"name": "75L AA 75/46",
"front_images": [ "it/gun/itAA7546.gif", "it/gun/itAA7546b.gif" ],
"back_images": [ "it/gun/itAA7546b.gif", "it/gun/itAA7546lb.gif" ]
},
"updated": {
"front_images": "it/gun/itAA7546.gif",
"back_images": "it/gun/itAA7546b.gif"
}
},
"3776": {
"expected": {
"name": "37* INF Skoda IG",
"front_images": [ "ax/gun/buIN37s.gif", "ax/gun/buIN37s2.gif" ],
"back_images": "ax/gun/buIN37sb.gif"
},
"updated": {
"front_images": "ax/gun/buIN37s.gif"
}
},
"3777": {
"expected": {
"name": "70* INF Skoda IG",
"front_images": [ "ax/gun/buIN37s.gif", "ax/gun/buIN37s2.gif" ],
"back_images": "ax/gun/buIN37sb.gif"
},
"updated": {
"front_images": "ax/gun/buIN37s2.gif"
}
},
"6802": {
"expected": {
"name": "20L (4) AA",
"front_images": [ "fi/gun/fi20L4 _2.png", "fi/gun/fi20L4 _2 LIM.png" ],
"back_images": null
},
"updated": {
"front_images": "fi/gun/fi20L4 _2.png"
}
},
"6803": {
"expected": {
"name": "20L VKT (12) AA",
"front_images": [ "fi/gun/fi20L12.png", "fi/gun/fi20L12L.png" ],
"back_images": null
},
"updated": {
"front_images": "fi/gun/fi20L12.png"
}
},
"6804": {
"expected": {
"name": "40L Bofors AA (s)",
"front_images": [ "fi/gun/fi40L.png", "fi/gun/fi40LL.png" ],
"back_images": null
},
"updated": {
"front_images": "fi/gun/fi40L.png"
}
},
"adf:1824": {
"expected": {
"name": "37L AT PTP obr. 30",
"front_images": "ru/gun/ruAT37L.gif",
"back_images": "ru/gun/ruAT37Lb.gif"
},
"updated": {
"front_images": "ru/gun/ru37LPTPobr30.png"
}
},
"adf:1822": {
"expected": {
"name": "37* INF PP obr. 15R",
"front_images": "ru/gun/ruINF37s.gif",
"back_images": "ru/gun/ruINF37sb.gif"
},
"updated": {
"front_images": "ru/gun/ru37PPobr15R.png"
}
},
"adf:1823": {
"expected": {
"name": "76* INF PP obr. 27",
"front_images": "ru/gun/ruINF76s.gif",
"back_images": "ru/gun/ruINF76sb.gif"
},
"updated": {
"front_images": "ru/gun/ru76PPobr27.png"
}
},
"3b5:10093": {
"expected": {
"name": "SL truck",
"front_images": [ "sh/SL3b(KFW).png", "sh/SL4b(KFW).png", "sh/SL5b(KFW).png", "sh/SL6b(KFW).png", "sh/SL1b(KFW).png", "sh/SL2b(KFW).png" ],
"back_images": [ "sh/SL3(KFW).png", "sh/SL4(KFW).png", "sh/SL5(KFW).png", "sh/SL6(KFW).png", "sh/SL1(KFW).png", "sh/SL2(KFW).png" ]
},
"updated": {
"front_images": "us/veh/usSearchlight(KFW).png",
"back_images": null
}
},
"08d:75": {
"expected": {
"name": "RCL 75*",
"front_images": "amrcl75-malf.png",
"back_images": "dm-75rcl.gif"
},
"updated": {
"front_images": "amrcl75.png"
}
}
}

@ -85,7 +85,7 @@
"notes": [ "a", "c", "f" ], "notes": [ "a", "c", "f" ],
"comments": [ "Road Rate NA" ], "comments": [ "Road Rate NA" ],
"id": "ge/v:009", "id": "ge/v:009",
"gpid": [ 2542, 7124 ] "gpid": [ 2542 ]
}, },
{ "name": "FT-17 730(f)", { "name": "FT-17 730(f)",
"type": "Tt", "type": "Tt",
@ -95,7 +95,7 @@
"notes": [ "a", "c", "f" ], "notes": [ "a", "c", "f" ],
"comments": [ "Road Rate NA" ], "comments": [ "Road Rate NA" ],
"id": "ge/v:010", "id": "ge/v:010",
"gpid": [ 2544, 7128 ] "gpid": [ 2544 ]
}, },
{ "name": "38H 735(f)", { "name": "38H 735(f)",
"type": "LT", "type": "LT",
@ -104,7 +104,7 @@
"note_number": "9.2", "note_number": "9.2",
"notes": [ "a", "b\u2020", "f" ], "notes": [ "a", "b\u2020", "f" ],
"id": "ge/v:011", "id": "ge/v:011",
"gpid": [ 2546, 7132 ] "gpid": [ 2546 ]
}, },
{ "name": "35-S 739(f)", { "name": "35-S 739(f)",
"type": "MT", "type": "MT",
@ -112,7 +112,7 @@
"note_number": "9.3", "note_number": "9.3",
"notes": [ "a", "b\u2020<sup>2</sup>", "d\u2020<sup>1</sup>", "f" ], "notes": [ "a", "b\u2020<sup>2</sup>", "d\u2020<sup>1</sup>", "f" ],
"id": "ge/v:012", "id": "ge/v:012",
"gpid": [ 2548, 7136 ] "gpid": [ 2548 ]
}, },
{ "name": "PzKpfw IIID", { "name": "PzKpfw IIID",
"type": "MT", "type": "MT",

@ -11,11 +11,10 @@ import urllib.error
import gzip import gzip
import time import time
import datetime import datetime
import tempfile
import logging import logging
from vasl_templates.webapp import app from vasl_templates.webapp import app, globvars
from vasl_templates.webapp.utils import parse_int from vasl_templates.webapp.utils import parse_int, friendly_byte_count
_registry = set() _registry = set()
_logger = logging.getLogger( "downloads" ) _logger = logging.getLogger( "downloads" )
@ -27,12 +26,11 @@ _etags = {}
class DownloadedFile: class DownloadedFile:
"""Manage a downloaded file.""" """Manage a downloaded file."""
def __init__( self, key, ttl, fname, url, on_data, extra_args=None ): def __init__( self, key, ttl, url, on_data, extra_args=None ):
# initialize # initialize
self.key = key self.key = key
self.ttl = ttl self.ttl = ttl
self.fname = fname
self.url = url self.url = url
self.on_data = on_data self.on_data = on_data
self.error_msg = None self.error_msg = None
@ -50,7 +48,7 @@ class DownloadedFile:
_registry.add( self ) _registry.add( self )
# check if we have a cached copy of the file # check if we have a cached copy of the file
self.cache_fname = os.path.join( tempfile.gettempdir(), "vasl-templates."+fname ) self.cache_fname = globvars.user_profile.downloaded_files[ self.key ]
if os.path.isfile( self.cache_fname ): if os.path.isfile( self.cache_fname ):
# yup - load it # yup - load it
_logger.info( "Using cached %s file: %s", key, self.cache_fname ) _logger.info( "Using cached %s file: %s", key, self.cache_fname )
@ -67,6 +65,8 @@ class DownloadedFile:
if len(data) < 1024 and os.path.isfile( data ): if len(data) < 1024 and os.path.isfile( data ):
with open( data, "r", encoding="utf-8" ) as fp: with open( data, "r", encoding="utf-8" ) as fp:
data = fp.read() data = fp.read()
if len( data ) == 0:
return
self._data = json.loads( data ) self._data = json.loads( data )
# notify the owner # notify the owner
if self.on_data: if self.on_data:
@ -92,7 +92,7 @@ class DownloadedFile:
self._lock.release() self._lock.release()
@staticmethod @staticmethod
def download_files(): def download_files(): #pylint: disable=too-many-locals
"""Download fresh copies of each file.""" """Download fresh copies of each file."""
#pylint: disable=protected-access #pylint: disable=protected-access
@ -117,7 +117,7 @@ class DownloadedFile:
_logger.info( "Download of the %s file has been disabled.", df.key ) _logger.info( "Download of the %s file has been disabled.", df.key )
continue continue
ttl *= 60*60 ttl *= 60*60
if os.path.isfile( df.cache_fname ): if os.path.isfile( df.cache_fname ) and os.stat( df.cache_fname ).st_size > 0:
# yup - check how long ago it was downloaded # yup - check how long ago it was downloaded
mtime = os.path.getmtime( df.cache_fname ) mtime = os.path.getmtime( df.cache_fname )
age = int( time.time() - mtime ) age = int( time.time() - mtime )
@ -144,10 +144,15 @@ class DownloadedFile:
with urllib.request.urlopen( req ) as resp: with urllib.request.urlopen( req ) as resp:
resp_data = resp.read() resp_data = resp.read()
if resp.headers.get( "Content-Encoding" ) == "gzip": if resp.headers.get( "Content-Encoding" ) == "gzip":
gzip_byte_count_str = " ({})".format( friendly_byte_count( len(resp_data) ) )
resp_data = gzip.decompress( resp_data ) resp_data = gzip.decompress( resp_data )
else:
gzip_byte_count_str = ""
data = resp_data.decode( "utf-8" ) data = resp_data.decode( "utf-8" )
etag = resp.headers.get( "ETag" ) etag = resp.headers.get( "ETag" )
_logger.info( "Downloaded the %s file OK: %d bytes", df.key, len(data) ) _logger.info( "Downloaded the %s file OK: %s", df.key,
friendly_byte_count( len(data) ) + gzip_byte_count_str
)
if etag: if etag:
_logger.debug( "- Got etag: %s", etag ) _logger.debug( "- Got etag: %s", etag )
_etags[ url ] = etag _etags[ url ] = etag

@ -3,6 +3,7 @@
from vasl_templates.webapp import app from vasl_templates.webapp import app
from vasl_templates.webapp.config.constants import APP_NAME, APP_VERSION from vasl_templates.webapp.config.constants import APP_NAME, APP_VERSION
user_profile = None
template_pack = None template_pack = None
vasl_mod = None vasl_mod = None
vo_listings = None vo_listings = None

@ -4,7 +4,7 @@ import os
import time import time
import base64 import base64
import logging import logging
import xml.etree.cElementTree as ET import xml.etree.ElementTree as ET
from flask import request, jsonify from flask import request, jsonify

@ -14,12 +14,12 @@ from flask import request, render_template, jsonify, send_file, redirect, url_fo
from vasl_templates.webapp import app, shutdown_event from vasl_templates.webapp import app, shutdown_event
from vasl_templates.webapp.vassal import VassalShim from vasl_templates.webapp.vassal import VassalShim
from vasl_templates.webapp.utils import MsgStore, get_java_version, parse_int from vasl_templates.webapp.utils import MsgStore, get_java_path, get_java_version, parse_int
import vasl_templates.webapp.config.constants import vasl_templates.webapp.config.constants
from vasl_templates.webapp.config.constants import BASE_DIR, DATA_DIR, IS_FROZEN from vasl_templates.webapp.config.constants import BASE_DIR, DATA_DIR, IS_FROZEN
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 from vasl_templates.webapp.lfa import DEFAULT_LFA_DICE_HOTNESS_WEIGHTS, DEFAULT_LFA_DICE_HOTNESS_THRESHOLDS
from vasl_templates.utils import get_build_info from vasl_templates.utils import get_build_info, get_build_git_info
# 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
@ -75,6 +75,8 @@ _APP_CONFIG_DEFAULTS = { # Bodhgaya, India (APR/19)
"ONLINE_COUNTER_IMAGES_URL_TEMPLATE": "https://raw.githubusercontent.com/vasl-developers/vasl/develop/dist/images/{{PATH}}", #pylint: disable=line-too-long "ONLINE_COUNTER_IMAGES_URL_TEMPLATE": "https://raw.githubusercontent.com/vasl-developers/vasl/develop/dist/images/{{PATH}}", #pylint: disable=line-too-long
"ONLINE_EXTN_COUNTER_IMAGES_URL_TEMPLATE": "http://vasl-templates.org/services/counter/{{EXTN_ID}}/{{PATH}}", "ONLINE_EXTN_COUNTER_IMAGES_URL_TEMPLATE": "http://vasl-templates.org/services/counter/{{EXTN_ID}}/{{PATH}}",
"ASA_UPLOAD_URL": "https://aslscenarioarchive.com/rest/update/{ID}?user={USER}&token={TOKEN}", "ASA_UPLOAD_URL": "https://aslscenarioarchive.com/rest/update/{ID}?user={USER}&token={TOKEN}",
"TURN_TRACK_SHADING_COLORS": [ "#e0e0e0", "#c0c0c0" ],
"SCENARIO_SEARCH_LOAD_BLOCK_SIZE": 1000, "SCENARIO_SEARCH_QUERY_INPUT_DELAY": 20,
} }
@app.route( "/app-config" ) @app.route( "/app-config" )
@ -85,7 +87,9 @@ def get_app_config():
"""Get a JSON value from the app config.""" """Get a JSON value from the app config."""
try: try:
val = app.config.get( key, default ) val = app.config.get( key, default )
return val if isinstance(val,dict) else json.loads(val) if isinstance( val, (dict,list) ):
return val
return json.loads( val )
except json.decoder.JSONDecodeError: except json.decoder.JSONDecodeError:
msg = "Couldn't parse app config setting: {}".format( key ) msg = "Couldn't parse app config setting: {}".format( key )
logging.error( "%s", msg ) logging.error( "%s", msg )
@ -99,6 +103,8 @@ def get_app_config():
} }
if isinstance( vals["THEATERS"], str ): if isinstance( vals["THEATERS"], str ):
vals["THEATERS"] = vals["THEATERS"].split() vals["THEATERS"] = vals["THEATERS"].split()
if isinstance( vals["TURN_TRACK_SHADING_COLORS"], str ):
vals["TURN_TRACK_SHADING_COLORS"] = vals["TURN_TRACK_SHADING_COLORS"].split()
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 )
@ -157,6 +163,61 @@ def get_app_config():
logging.error( "%s", msg ) logging.error( "%s", msg )
startup_msg_store.error( msg ) startup_msg_store.error( msg )
# include the Trumbowyg config
# NOTE: We don't include the "insertImage" button because it doesn't seem to work when
# the Trumbowyg control is in a dialog, and given VASSAL's handling of images, we don't
# really want to be encouraging their use :-/
vals[ "trumbowyg" ] = {
# NOTE: Trumbowyg only allows tags to be black-listed, not attributes, but since we mostly
# do HTML sanitization using DOMPurify when loading/unloading the UI, what we configure here
# is only used to protect against the case where the user enters some malicious HTML into
# the editor i.e. the only risk is that they mess up their own session :-/
"tag-blacklist": get_json_val( "TRUMBOWYG_TAG_BLACKLIST", [
"script", "iframe", "form", "link", "style", "meta", "object", "applet",
"audio", "base", "bgsound", "embed", "isindex", "keygen", "layer", "svg", "template", "video",
] ),
"format-options": get_json_val( "TRUMBOWYG_FORMAT_OPTIONS", [
"h1", "h2", "h3",
] ),
"special-chars": get_json_val( "TRUMBOWYG_SPECIAL_CHARS", [
"2264", "2265", "2260", "00d7", "00f7", None, # math
"00bd", "00bc", "00be", "215b", "215c", "215d", "215e", None, # fractions
"25b3", "24c7", "24b7", "24ba", "2605", "221e", "b0", "b7", None, # special
"e4", "eb", "ef", "f6", "fc", "c4", "cb", "cf", "d6", "dc", None, # umlaut
"e1", "e9", "ed", "f3", "fa", "c1", "c9", "cd", "d3", "da", None, # acute
"e0", "e8", "ec", "f2", "f9", "c0", "c8", "cc", "d2", "d9", None, # grave
"e2", "ea", "ee", "f4", "fb", "c2", "ca", "ce", "d4", "db", None, # circumflex
"1f850", "1f852", "1f851", "1f853", # arrows
] ),
"victory-conditions": get_json_val( "TRUMBOWYG_BUTTONS_VICTORY_CONDITIONS", [
[ "strong", "em", "underline", "superscript", "subscript", "format" ],
[ "foreColor", "backColor", "fontfamily", "fontsize" ],
[ "outdent", "indent" ],
[ "unorderedList", "orderedList", "table" ],
[ "specialChars", "flags", "emoji" ],
[ "removeformat", "historyUndo", "historyRedo", "viewHTML", "fullscreen" ],
] ),
# NOTE: I tried having different buttons for OB setup notes (which tend to be simpler)
# and OB notes (which can be more involved), but (1) this meant we had to tear down
# and re-create the Trumbowyg control each time the dialog was opened (since there doesn't
# seem to be any way to dynamically change the buttons), which caused a noticeable delay,
# and (2) users are probaly going to complain :-/
"simple-note-dialog": get_json_val( "TRUMBOWYG_BUTTONS_SIMPLE_NOTE_DIALOG", [
[ "strong", "em", "underline", "del", "superscript", "subscript", "format" ],
[ "foreColor", "backColor", "fontfamily", "fontsize" ],
[ "align", "outdent", "indent" ],
[ "unorderedList", "orderedList", "table" ],
[ "specialChars", "flags", "emoji" ],
[ "removeformat", "historyUndo", "historyRedo", "viewHTML", "fullscreen" ],
] ),
"html-textbox-dialog": get_json_val( "TRUMBOWYG_BUTTONS_HTML_TEXTBOX_DIALOG", [
[ "strong", "em", "underline", "del", "superscript", "subscript" ],
[ "foreColor", "backColor" ],
[ "specialChars", "flags", "emoji" ],
[ "removeformat", "historyUndo", "historyRedo", "viewHTML", "fullscreen" ],
] ),
}
return jsonify( vals ) return jsonify( vals )
# --------------------------------------------------------------------- # ---------------------------------------------------------------------
@ -174,16 +235,24 @@ def get_program_info():
return tstamp + timedelta( minutes=tz_offset ) return tstamp + timedelta( minutes=tz_offset )
# set the basic details # set the basic details
try:
vassal_version = VassalShim.get_version()
except Exception: #pylint: disable=broad-except
vassal_version = "???"
params = { params = {
"APP_VERSION": vasl_templates.webapp.config.constants.APP_VERSION, "APP_VERSION": vasl_templates.webapp.config.constants.APP_VERSION,
"VASSAL_VERSION": VassalShim.get_version() "VASSAL_VERSION": vassal_version
} }
build_git_info = get_build_git_info()
if build_git_info:
params[ "BUILD_GIT_INFO" ] = build_git_info
if globvars.vasl_mod: if globvars.vasl_mod:
params[ "VASL_VERSION" ] = globvars.vasl_mod.vasl_version params[ "VASL_VERSION" ] = globvars.vasl_mod.vasl_version
params[ "VASL_REAL_VERSION" ] = globvars.vasl_mod.vasl_real_version params[ "VASL_REAL_VERSION" ] = globvars.vasl_mod.vasl_real_version
for key in [ "VASSAL_DIR", "VASL_MOD", "VASL_EXTNS_DIR", "BOARDS_DIR", for key in [ "VASSAL_DIR", "VASL_MOD", "VASL_EXTNS_DIR", "BOARDS_DIR",
"JAVA_PATH", "WEBDRIVER_PATH", "CHAPTER_H_NOTES_DIR", "USER_FILES_DIR" ]: "WEBDRIVER_PATH", "CHAPTER_H_NOTES_DIR", "USER_FILES_DIR" ]:
params[ key ] = app.config.get( key ) params[ key ] = app.config.get( key )
params[ "JAVA_PATH" ] = get_java_path()
params[ "JAVA_VERSION" ] = get_java_version() params[ "JAVA_VERSION" ] = get_java_version()
def parse_timestamp( val ): def parse_timestamp( val ):
@ -211,16 +280,14 @@ def get_program_info():
to_localtime( datetime.utcfromtimestamp( build_info["timestamp"] ) ), to_localtime( datetime.utcfromtimestamp( build_info["timestamp"] ) ),
"%H:%M (%d %b %Y)" "%H:%M (%d %b %Y)"
) )
params[ "BUILD_GIT_INFO" ] = build_info[ "git_info" ]
# check if we are running inside a Docker container # check if we are running inside a Docker container
if app.config.get( "IS_CONTAINER" ): if app.config.get( "IS_CONTAINER" ):
# yup - return related information # yup - return related information
params[ "BUILD_GIT_INFO" ] = os.environ.get( "BUILD_GIT_INFO" )
params[ "DOCKER_IMAGE_NAME" ] = os.environ.get( "DOCKER_IMAGE_NAME" ) params[ "DOCKER_IMAGE_NAME" ] = os.environ.get( "DOCKER_IMAGE_NAME" )
params[ "DOCKER_IMAGE_TIMESTAMP" ] = datetime.strftime( params[ "DOCKER_IMAGE_TIMESTAMP" ] = datetime.strftime(
parse_timestamp( os.environ.get( "DOCKER_IMAGE_TIMESTAMP" ) ), parse_timestamp( os.environ.get( "DOCKER_IMAGE_TIMESTAMP" ) ),
"%H:%M %d %b %Y" "%d %b %Y %H:%M"
) )
params[ "DOCKER_CONTAINER_NAME" ] = os.environ.get( "DOCKER_CONTAINER_NAME" ) params[ "DOCKER_CONTAINER_NAME" ] = os.environ.get( "DOCKER_CONTAINER_NAME" )
with open( "/proc/self/cgroup", "r", encoding="utf-8" ) as fp: with open( "/proc/self/cgroup", "r", encoding="utf-8" ) as fp:
@ -252,7 +319,9 @@ def get_program_info():
check_df( _asa_scenarios ) check_df( _asa_scenarios )
check_df( _roar_scenarios ) check_df( _roar_scenarios )
return render_template( "program-info-content.html", **params ) if request.args.get( "f" ) == "html":
return render_template( "program-info-content.html", **params )
return jsonify( params )
# --------------------------------------------------------------------- # ---------------------------------------------------------------------

@ -41,7 +41,8 @@ def main( bind_addr, force_init_delay, flask_debug ):
# monitor extra files for changes # monitor extra files for changes
extra_files = [] extra_files = []
fspecs = [ "static/", "templates/", "config/" ] fspecs = [ "static/", "static/css/", "static/help/", "templates/", "config/" ]
fspecs.extend( [ "data/default-template-pack/", "data/default-template-pack/extras/" ] )
fspecs.extend( [ "tests/control_tests_servicer.py", "tests/proto/generated/" ] ) fspecs.extend( [ "tests/control_tests_servicer.py", "tests/proto/generated/" ] )
for fspec in fspecs: for fspec in fspecs:
fspec = os.path.abspath( os.path.join( os.path.dirname(__file__), fspec ) ) fspec = os.path.abspath( os.path.join( os.path.dirname(__file__), fspec ) )

@ -10,7 +10,6 @@ import base64
import re import re
import time import time
import math import math
import hashlib
import logging import logging
from flask import request, render_template, make_response, jsonify, abort from flask import request, render_template, make_response, jsonify, abort
@ -32,6 +31,8 @@ def _build_asa_scenario_index( df, new_data, logger ):
scenario["scenario_id"]: scenario scenario["scenario_id"]: scenario
for scenario in new_data["scenarios"] for scenario in new_data["scenarios"]
} }
for scenario in index.values():
_tidyup_strings( scenario )
# install the results # install the results
df.index = index df.index = index
df.generated_at = new_data.get( "_generatedAt_" ) df.generated_at = new_data.get( "_generatedAt_" )
@ -39,8 +40,13 @@ def _build_asa_scenario_index( df, new_data, logger ):
logger.debug( "Loaded the ASL Secenario Archive index: #scenarios=%d", len(df.index) ) logger.debug( "Loaded the ASL Secenario Archive index: #scenarios=%d", len(df.index) )
logger.debug( "- Generated at: %s", new_data.get( "_generatedAt_", "n/a" ) ) logger.debug( "- Generated at: %s", new_data.get( "_generatedAt_", "n/a" ) )
def _tidyup_strings( scenario ):
"""Tidy up strings in scenario records."""
for key, val in scenario.items():
if isinstance( val, str ):
scenario[key] = val.strip()
_asa_scenarios = DownloadedFile( "ASA", 6, # nb: TTL = #hours _asa_scenarios = DownloadedFile( "ASA", 6, # nb: TTL = #hours
"asl-scenario-archive.json",
"https://vasl-templates.org/services/asl-scenario-archive/scenario-index.json", "https://vasl-templates.org/services/asl-scenario-archive/scenario-index.json",
_build_asa_scenario_index, _build_asa_scenario_index,
extra_args = { "index": None } extra_args = { "index": None }
@ -56,6 +62,7 @@ def _build_roar_scenario_index( df, new_data, logger ):
if roar_id.startswith( "_" ): if roar_id.startswith( "_" ):
continue continue
scenario[ "roar_id" ] = roar_id scenario[ "roar_id" ] = roar_id
_tidyup_strings( scenario )
index[ roar_id ] = scenario index[ roar_id ] = scenario
_update_roar_matching_index( title_matching, scenario.get("name"), roar_id ) _update_roar_matching_index( title_matching, scenario.get("name"), roar_id )
_update_roar_matching_index( id_matching, scenario.get("scenario_id"), roar_id ) _update_roar_matching_index( id_matching, scenario.get("scenario_id"), roar_id )
@ -84,7 +91,6 @@ def _make_roar_matching_key( val ):
return re.sub( "[^a-z0-9]", "", val.lower() ) return re.sub( "[^a-z0-9]", "", val.lower() )
_roar_scenarios = DownloadedFile( "ROAR", 6, # nb: TTL = #hours _roar_scenarios = DownloadedFile( "ROAR", 6, # nb: TTL = #hours
"roar-scenario-index.json",
"https://vasl-templates.org/services/roar/scenario-index.json", "https://vasl-templates.org/services/roar/scenario-index.json",
_build_roar_scenario_index, _build_roar_scenario_index,
extra_args = { "index": None } extra_args = { "index": None }
@ -124,7 +130,15 @@ def get_scenario_index():
return _make_not_available_response( return _make_not_available_response(
"Please wait, the scenario index is still downloading.", None "Please wait, the scenario index is still downloading.", None
) )
etag = hashlib.md5( json.dumps( _asa_scenarios.index ).encode( "utf-8" ) ).hexdigest() # NOTE: We used to calculate the ETag like this:
# etag = hashlib.md5( json.dumps( _asa_scenarios.index ).encode( "utf-8" ) ).hexdigest()
# but this is slow, and is done *every* time i.e. even when nothing's changed and we return a 304 :-/
# So, we take advantage of the fact that the only time the index changes is when we've downloaded
# a new version, and the *entire* dict changes i.e. we can use id() to detect if anything's changed.
# The only exception to this is when we temporarily modify the index ourself after uploading
# a scenario to the ASL Scenario Archive, so in that case, we clone the dict to ensure that
# id() returns something different.
etag = str( id( _asa_scenarios.index ) )
if request.headers.get( "If-None-Match" ) == etag: if request.headers.get( "If-None-Match" ) == etag:
return "Not Modified", 304 return "Not Modified", 304
resp = make_response( jsonify( [ resp = make_response( jsonify( [
@ -147,7 +161,8 @@ def get_roar_scenario_index():
return _make_not_available_response( return _make_not_available_response(
"Please wait, the ROAR scenarios are still downloading.", None "Please wait, the ROAR scenarios are still downloading.", None
) )
etag = hashlib.md5( json.dumps( _roar_scenarios.index ).encode( "utf-8" ) ).hexdigest() # NOTE: See get_scenario_index() for why we calculate the ETag in this way.
etag = str( id( _roar_scenarios.index ) )
if request.headers.get( "If-None-Match" ) == etag: if request.headers.get( "If-None-Match" ) == etag:
return "Not Modified", 304 return "Not Modified", 304
resp = make_response( jsonify( _roar_scenarios.index ) ) resp = make_response( jsonify( _roar_scenarios.index ) )
@ -177,6 +192,11 @@ def get_scenario( scenario_id ): #pylint: disable=too-many-locals
args[ "attacker_name" ] = scenario.get( "attacker" ) args[ "attacker_name" ] = scenario.get( "attacker" )
args = { k.lower(): v for k,v in args.items() } args = { k.lower(): v for k,v in args.items() }
# get the number of turns
nturns = scenario.get( "max_turns" ) or scenario.get( "min_turns" )
if nturns:
args[ "scenario_turns" ] = nturns
def get_win_score( key ): def get_win_score( key ):
"""Get a player's win percentage.""" """Get a player's win percentage."""
nWins = parse_int( playings.get( key+"_wins" ), -1 ) nWins = parse_int( playings.get( key+"_wins" ), -1 )
@ -519,7 +539,7 @@ def prepare_asa_upload(): #pylint: disable=too-many-locals
max_size = parse_int( app.config.get( "ASA_MAX_SCREENSHOT_SIZE" ), 200 ) * 1024 max_size = parse_int( app.config.get( "ASA_MAX_SCREENSHOT_SIZE" ), 200 ) * 1024
if len(screenshot_data) > max_size: if len(screenshot_data) > max_size:
ratio = math.sqrt( float(max_size) / len(screenshot_data) ) ratio = math.sqrt( float(max_size) / len(screenshot_data) )
img = img.resize( ( int(img.width * ratio), int(img.height * ratio) ), Image.ANTIALIAS ) img = img.resize( ( int(img.width * ratio), int(img.height * ratio) ), Image.LANCZOS )
# add a border # add a border
border_size = parse_int( app.config.get( "ASA_SCREENSHOT_BORDER_SIZE" ), 5 ) border_size = parse_int( app.config.get( "ASA_SCREENSHOT_BORDER_SIZE" ), 5 )
img = ImageOps.expand( img, border_size, (255,255,255,255) ) img = ImageOps.expand( img, border_size, (255,255,255,255) )
@ -586,6 +606,8 @@ def on_successful_asa_upload( scenario_id ):
# update the in-memory scenario index # update the in-memory scenario index
with _asa_scenarios: with _asa_scenarios:
# NOTE: We clone the index so that it will get a new ETag in get_scenario_index().
_asa_scenarios.index = dict( _asa_scenarios.index )
_asa_scenarios.index[ scenario_id ] = new_scenario _asa_scenarios.index[ scenario_id ] = new_scenario
return jsonify( { "status": "ok" } ) return jsonify( { "status": "ok" } )

@ -191,13 +191,27 @@ def make_snippet_image():
def get_flag( nat ): def get_flag( nat ):
"""Get a flag image.""" """Get a flag image."""
# NOTE: Serving flag images should be so easy :-/ Normally, we would just return the appropriate image file,
# and let the client resize it to whatever it wants. However, VASSAL does a fairly bad job of this, so we allow
# the client to specify a preferred height, and we resize the image ourself. This gives slightly better results
# (although there's only so much you can do when your images are 11x11 :-/), but it's slow, and there are still
# some that don't look great (e.g. the German flag), so we allow the client to specify a preferred height, and
# if we have a prepared file of that size, we just return that directly.
# The downside of all this tomfoolery is when images are being downloaded from the internet, since the server
# needs to handle pre-sized files, based on a query parameter. The RewriteRule is funky, but it works :-/
# NOTE: We could insert the preferred size into the URL (e.g. "$/flags/11/german"), but this takes control away
# from the templates wrt what goes into the labels, and won't work for custom flag URL's. It's probably cleaner
# and safer over the longer term to just have a single URL for each flag, and tweak it with something that
# won't break things if it's not there (i.e. a query parameter). At worst, the default image will be served
# and things will just look a bit bad, but it won't 404 :-/
# initialize # initialize
if not re.search( "^[-a-z~]+$", nat ): if not re.search( "^[-a-z~]+$", nat ):
abort( 404 ) abort( 404 )
key = "flags:{}".format( nat ) key = "flags:{}".format( nat )
# NOTE: Most of the flags are at the larger size, so we default to that size (since we get better results height = int( request.args.get( "prefh",
# doing that, and scaling down to the smaller size as needed, rather than the other way around). app.config.get( "DEFAULT_FLAG_HEIGHT", 13 )
height = app.config.get( "DEFAULT_FLAG_HEIGHT", 13 ) ) )
# check if a custom flag has been configured # check if a custom flag has been configured
if globvars.template_pack: if globvars.template_pack:
@ -205,40 +219,50 @@ def get_flag( nat ):
if fname: if fname:
if fname.startswith( ("http://","https://") ): if fname.startswith( ("http://","https://") ):
with urllib.request.urlopen( fname ) as resp: with urllib.request.urlopen( fname ) as resp:
return _get_small_image( resp, key, height ) return _get_resized_image( resp, key, height )
else: else:
with open( fname, "rb" ) as fp: with open( fname, "rb" ) as fp:
return _get_small_image( fp, key, height ) return _get_resized_image( fp, key, height )
# serve the standard flag # serve the standard flag
fname = os.path.join( "static/images/flags/", nat+".png" ) # NOTE: Some flags don't reduce so well (e.g. German), so we allow for pre-reduced versions.
# This means that for labels that get images from the internet, we need some funky RewriteRule's
# on the online server, so that it can check the "height" query parameter, and serve the correct file.
# We could try to tweak image URL's so that the height is embedded in them (e.g. $/flags/11/german),
# but this is fiddly, and doesn't play well with custom flag URL's i.e. we're better off having
# a single base URL for each flag, and then tweaking it via query parameters.
fname = "static/images/flags/{}-{}.png".format( nat, height )
if not os.path.isfile( os.path.join( app.root_path, fname ) ):
fname = "static/images/flags/{}.png".format( nat )
try: try:
with app.open_resource( fname, "rb" ) as fp: with app.open_resource( fname, "rb" ) as fp:
return _get_small_image( fp, key, height ) return _get_resized_image( fp, key, height )
except FileNotFoundError: except FileNotFoundError:
if nat in globvars.template_pack["nationalities"] and not request.args.get("no-spacer"):
# NOTE: If the nationalitity is valid but has no flag, we return a spacer image, so that
# the scenario card doesn't show a "broken image" image.
fname = os.path.join( "static/images/spacer.png" )
return send_file( fname )
abort( 404 ) abort( 404 )
return None # stop pylint from complaining :-/ return None # stop pylint from complaining :-/
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
_small_image_cache = {} _resized_image_cache = {}
_small_image_cache_lock = threading.Lock() _resized_image_cache_lock = threading.Lock()
def _get_small_image( fp, key, default_height ): def _get_resized_image( fp, key, height ):
"""Get a small image (cached).""" """Get a resized image (cached)."""
# check how we should resize the image # initialize
# NOTE: Resizing images in the HTML snippets looks dreadful (presumably
# because VASSAL's HTML engine is so ancient), so we do it ourself :-/
height = int( request.args.get( "height", default_height ) )
if height <= 0: if height <= 0:
abort( 400 ) abort( 400 )
with _small_image_cache_lock: with _resized_image_cache_lock:
# check if we have the image in the cache # check if we have the image in the cache
cache_key = ( key, height ) cache_key = ( key, height )
if cache_key not in _small_image_cache: if cache_key not in _resized_image_cache:
# nope - load it # nope - load it
img = Image.open( fp ) img = Image.open( fp )
@ -247,13 +271,13 @@ def _get_small_image( fp, key, default_height ):
if height > 0: if height > 0:
width = img.size[0] / ( float(img.size[1]) / height ) width = img.size[0] / ( float(img.size[1]) / height )
width = int( width + 0.5 ) width = int( width + 0.5 )
img = img.resize( (width,height), Image.ANTIALIAS ) img = img.resize( (width,height), Image.Resampling.LANCZOS )
# add the image to the cache # add the image to the cache
buf = io.BytesIO() buf = io.BytesIO()
img.save( buf, format="PNG" ) img.save( buf, format="PNG" )
buf.seek( 0 ) buf.seek( 0 )
_small_image_cache[ cache_key ] = buf.read() _resized_image_cache[ cache_key ] = buf.read()
# return the flag image # return the flag image
img_data =_small_image_cache[ cache_key ] img_data =_resized_image_cache[ cache_key ]
return send_file( io.BytesIO(img_data), mimetype="image/png" ) return send_file( io.BytesIO(img_data), mimetype="image/png" )

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -0,0 +1,5 @@
/* NOTE: This stylesheet will be dynamically injected into the UI when custom list bullets are enabled. */
.trumbowyg-editor ul { list-style-image: url("../images/bullet.png") ; }
.trumbowyg-editor ul ul { list-style-image: url("../images/bullet2.png") ; }
.trumbowyg-editor ul ul ul { list-style-image: url("../images/bullet3.png") ; }
.trumbowyg-editor ol { list-style-image: none ; }

@ -0,0 +1,11 @@
/* NOTE: This file contains work-arounds for the desktop app. */
fieldset[name='scenario'] { max-height: 251px ; }
/* these work around vertical alignment of text in input controls */
#panel-scenario label { margin-bottom: -1px ; }
#panel-scenario label[for="TURN_TRACK_NTURNS"], #panel-scenario label[for="PLAYER_1"], #panel-scenario label[for="PLAYER_2"] {
line-height: 20px ;
}
#panel-scenario input[name="SCENARIO_DATE"] { padding-top: 2px ; }

@ -0,0 +1,4 @@
#edit-html_textbox-dialog { overflow: hidden ; padding-bottom: 0.75em !important ; }
.ui-dialog.edit-html_textbox .ui-dialog-buttonpane { border: none ; margin-top: 0 !important ; padding-top: 0 !important ; }
.ui-dialog.edit-html_textbox .container { height: 100% ; display: flex ; flex-direction: row ; }

@ -1,6 +1,6 @@
#edit-simple_note { overflow: hidden ; } #edit-simple_note { overflow: hidden ; padding-bottom: 0.75em !important ; }
.ui-dialog.edit-simple_note .ui-dialog-buttonpane { border: none ; margin-top: 0 !important ; padding-top: 0 !important ; } .ui-dialog.edit-simple_note .ui-dialog-buttonpane { border: none ; margin-top: 0 !important ; padding-top: 0 !important ; }
.ui-dialog.edit-simple_note textarea { resize: none ; width: calc(100% - 4px) ; height: 100% ; } .ui-dialog.edit-simple_note .container { height: 100% ; display: flex ; flex-direction: row ; }
.ui-dialog.edit-simple_note label[for="width"] { color: #444 ; } .ui-dialog.edit-simple_note label[for="width"] { color: #444 ; }
.ui-dialog.edit-simple_note input[name="width"] { color: #444 ; } .ui-dialog.edit-simple_note input[name="width"] { color: #444 ; }

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save