code:
# Contributing to FC: Pregmod

First off, thanks for taking the time to contribute!

If there is anything you don't understand feel free to ask. Many of the more advanced tools are also not required for
fixing small typos or simple bugs.

## Environment

### Requirements

To effectively work on the project the following tools are required:

* `Git`
* `Node.js` or another npm client
* an IDE capable of working with JavaScript, TypeScript and CSS. [VS Code](https://code.visualstudio.com/) is one option.
* Java Runtime Environment, minimum JRE8

### Setting everything up

0. Clone the project from GitGud.io ([Detailed Git setup and work cycle](devNotes/gitSetup.md))
1. Navigate to the `fc-pregmod` root directory.
   * Windows: Open the cmd/Powershell and execute `cd C:\path\to\project\fc-pregmod`
   * GNU/Linux: Open a terminal and execute `cd /path/to/project/fc-pregmod/`.
2. Run `npm install`
3. Open the directory in your preferred IDE
4. Configure your IDE to use ESLint.
5. Recommended extensions for VSCode:
   * [Code Spell Checker](https://marketplace.visualstudio.com/items?itemName=streetsidesoftware.code-spell-checker)
   * [GitLens — Git supercharged](https://marketplace.visualstudio.com/items?itemName=eamodio.gitlens).

## Compiling

While you can compile it like usual (`compile.bat`/`compile.sh`/`make`), there is also a `Gulp script` that creates
source maps for easier debugging. Other than that there are no differences between compiling for development or
compiling for playing the game.

# Code

## Code style

Generally the code style is based on our `.eslintrc.json`. If your IDE has an auto format feature it can often read the
rules from `.eslintrc.json`.

### Important Rules

* use spaces after commas
* do not omit semicolons
* no empty blocks
* don't pad blocks with blank lines
* prefer strict equality/inequality
* etc.

### Documentation

It's a good idea to provide meaningful documentation for new functions and classes where possible. We follow
Typescript's [JSDoc](https://jsdoc.app) type dialect for the most part (and we provide a Typescript configuration and
auxiliary type definition files if you'd like to use it yourself – it's pretty nifty). Don't worry too much about
specific type syntax if you can't make TS work or don't understand it, someone else will probably fix it for you as long
as you've made the intent clear in some form of JSDoc.

### Naming conventions

* JavaScript variable and function names should be `camelCase`.

```js
// good
let fooBar;

// bad
let foobar;
let Foobar;
let FooBar;
```

* JavaScript classes and namespaces should be `PascalCase`.

```js
// good
class Foo {}
App.Foo.bar();

// bad
class foo {}
class FOO {}
App.foo.bar();
```

* Enum members should be `ALLCAPS`.

```ts
// good
enum Foo {
  BAR = 'bar',
  BAZ = 'baz',
}

// bad
enum Foo {
  bar = 'bar',
  Baz = 'baz',
}
```

This also applies to JavaScript objects that are used as enums.

```js
// good
/** @enum {string} */
const Foo = {
  BAR: 'bar',
  BAZ: 'baz',
}

// bad
/** @enum {string} */
const foo = {
  BAR: 'bar',
  BAZ: 'baz',
}

// worse
/** @enum {string} */
const foo = {
  bar: 'bar',
  Baz: 'baz',
}
```

* CSS classes are `kebob-case`.

```css
/* good */
.foo-bar {}

/* bad */
.fooBar {}
.FOO-BAR {}
```

New code should generally get organized into the `App` namespace. See [fc-js-init.js](js/002-config/fc-js-init.js) for a rough outline.

## JavaScript Features

* Avoid using very new JavaScript features
  * Generally, we're currently targeting ECMAScript 2018, though we use a few widely-implemented ECMAScript 2019
    features, like `globalThis`.
* Conversely, do use modern features, it's not 2010 anymore and we don't try to support Internet Explorer or anything
  stupid like that.
  * use `let`/`const` rather than `var`
  * prefer fat arrow functions to inline long-form functions
  * etc.

## Code quality

There are three main tools used to ensure good code quality, `ESLint`, `TypeScript Compiler (tsc)` and a custom sanity
check.

`ESLint` and `tsc` can be setup in most IDEs aimed at web development to show errors while editing the file.

Contributions should generally not add any new sanity check errors, and it's especially important to check this if
you're making changes to .tw files.
Use `./compile.sh --dry --sanity` or the short hand `./compile.sh -d -s` on linux or mac.
On Windows run `compile_debug+sanityCheck.bat`.
It searches for common spelling errors and syntax errors in the twine files. Don't worry about preexisting
errors, it's not totally clean as is and there are a few false positives.

## Project Structure

### Source files

* `src/` is the main directory for source code. It also contains the embedded art files. Since it is inside an `eval()` statement debugging and minification is more complicated than for `js/`.
* `js/` is loaded before SugarCube and therefore outside SugarCube's eval which `src/` is contained in. This means accessing SugarCube features (like `SugarCube.Engine`) is more complicated however it other than `src/` it can be minified and is easier to debug. Currently contains mainly static data, however new code not relying on SC2 should be sorted in here too.
* `css/` contains all CSS files. The internal structure is explained [here](css/css.md).
* `themes/` contains [custom themes](themes/themes.md) which are built separately.

### Dev Files

* `devNotes/` contains various wiki files and other potentially interesting files.
* `devTools/` contains various scripts and executables needed for working with the project or compiling. TypeScript typing files are stored here as well.
* `submodules/` contains git submodules. Only the custom version of SugarCube2 right now.

### Art files

* `artTools/`contains files for creating and updating [vector art](artTools/README.md).
* `resources/` contains various [art related files](resources/ReadmeBeforeAddingArt.md).

### External Programs
* `docker/` contains Docker files from which the docker containers for GitLabs CI are created.
* `FCHost/` contains the sources for [FCHost](FCHost/documentation_FCHost.md).
* `saveTools/` contains tools for [editing save files](saveTools/README.md).

# Further Reading

## Wiki Files

* Event Writing Guides
  * [Twine](devNotes/scene-guide.txt) (Twine is being phased out of the project however the concepts are still relevant)
  * [JavaScript](devNotes/jsEventCreationGuide.md)
* [Exception handling](devNotes/exceptions.md)
* [Sanity check](devNotes/sanityCheck.md)
* [Slave List](devNotes/slaveListing.md)
* [Pronouns](devNotes/Pronouns.md)
* External function documentation
  * [Eyes](devNotes/eye functions.md)
  * [Limbs](devNotes/limb functions.md)
  * [Standalone functions](devNotes/standaloneFunctions.md)
  * [Some potentially useful JS functions](devNotes/usefulJSFunctionDocumentation.txt)
  * [Content embedding chart](devNotes/contentEmbeddingChart.png) (for more details refer to [this issue](https://gitgud.io/pregmodfan/fc-pregmod/-/merge_requests/7453#note_118616))

## Useful issues

* [Setting up VS Code](https://gitgud.io/pregmodfan/fc-pregmod/-/issues/2448)
* [Classes in Game State](https://gitgud.io/pregmodfan/fc-pregmod/-/issues/696)
* [Self executing functions](https://gitgud.io/pregmodfan/fc-pregmod/-/issues/2325)
* [Sort a map](https://gitgud.io/pregmodfan/fc-pregmod/-/issues/2642)

repo_name: Banyanael/fc

license: mit


code:
CMakeCache.txt
CMakeFiles
Debug
Release
x64
*.sln
*.vcxproj
*.vcxproj.filters
*.vcxproj.user
libcef_dll_wrapper
.vs
cmake_install.cmake
*.VC.db
GPUCache


code:
BUILD REQUIREMENTS

The below requirements must be met to build this project.

 - Chromium Embedded Framework (CEF) 76.1.5 or newer.  The windows build is
   currently configured to operate with the X64 binary distribution of CEF.

 - CMake version 2.8.12.1 or newer.

 - Linux/MacOS requirements:
   I haven't tested these platforms, and there is platform-specific code missing.
   Shouldn't be too hard to add, check the CEF distribution and treat FCHost
   as a derivative of tests/cefsimple.

 - Have the environment variable CEF_ROOT set to the root of your CEF distribution.
 - Have the CMake binary folder added to your path e.g. for Windows C:\Program Files\CMake\bin.

 - Windows requirements:
   Visual Studio 2019 (the community edition will work fine) or newer with the "Desktop development with C++" workload installed.

BUILD STEPS

If you are running Windows and meet the above requirements then within the root of FCHost/,
execute cmake_vs2019.bat which will generate FCHost.sln in the same location.
Now open FCHost.sln in Visual Studio 2019
and build from there via tool bar -> Build -> Build Solution or Ctrl + Shift + B.

The resulting binary and supporting files will be in
FCHost/fchost/Debug, or FCHost/fchost/Release depending on the configuration
selected in Visual Studio via the drop down found under the Build and Debug tool bar headers.

You should be able to zip up the contents of the
Release folder and distribute them to another machine or user if desired.
Due to space and privacy concerns it is suggested that you remove *.html and fchost.ilk prior to distribution.

If you are not running Windows or you need something else special, please
familiarize yourself with CMake and see the detailed information in CMakeLists.txt.


code:
# Copyright (c) 2014 The Chromium Embedded Framework Authors. All rights
# reserved. Use of this source code is governed by a BSD-style license that
# can be found in the LICENSE file.

# OVERVIEW
#
# CMake is a cross-platform open-source build system that can generate project
# files in many different formats. It can be downloaded from
# http://www.cmake.org or installed via a platform package manager.
#
# CMake-generated project formats that have been tested with this CEF binary
# distribution include:
#
# Linux:      Ninja, Unix Makefiles
# Mac OS X:   Ninja, Xcode 5+
# Windows:    Ninja, Visual Studio 2010+
#
# Ninja is a cross-platform open-source tool for running fast builds using
# pre-installed platform toolchains (GNU, clang, Xcode or MSVC). It can be
# downloaded from http://martine.github.io/ninja/ or installed via a platform
# package manager.
#
# BUILD REQUIREMENTS
#
# The below requirements must be met to build this project.
#
# - Chromium Embedded Framework (CEF) 89.0 or newer.
#
# - CMake version 2.8.12.1 or newer.
#
# - Linux/MacOS requirements:
#   I haven't tested these platforms. Shouldn't be too hard to add, check the CEF
#   distribution and treat FCHost like tests/cefsimple (from which it is derived).
#
# - Windows requirements:
#   Visual Studio 2019 or newer.
#
# BUILD EXAMPLES
#
# The below commands will generate project files and create a Debug build of all
# CEF targets using CMake and the platform toolchain.
#
# Start by creating and entering the CMake build output directory:
# > cd path/to/cef_binary_*
# > mkdir build && cd build
#
# To perform a Linux build using a 32-bit CEF binary distribution on a 32-bit
# Linux platform or a 64-bit CEF binary distribution on a 64-bit Linux platform:
#   Using Unix Makefiles:
#     > cmake -G "Unix Makefiles" -DCMAKE_BUILD_TYPE=Debug ..
#     > make -j4 cefclient cefsimple
#
#   Using Ninja:
#     > cmake -G "Ninja" -DCMAKE_BUILD_TYPE=Debug ..
#     > ninja cefclient cefsimple
#
# To perform a Mac OS X build using a 64-bit CEF binary distribution:
#   Using the Xcode IDE:
#     > cmake -G "Xcode" -DPROJECT_ARCH="x86_64" ..
#     Open build\cef.xcodeproj in Xcode and select Product > Build.
#
#   Using Ninja:
#     > cmake -G "Ninja" -DPROJECT_ARCH="x86_64" -DCMAKE_BUILD_TYPE=Debug ..
#     > ninja cefclient cefsimple
#
# To perform a Windows build using a 32-bit CEF binary distribution:
#   Using the Visual Studio 2019 IDE:
#     > cmake -G "Visual Studio 16" ..
#     Open build\cef.sln in Visual Studio and select Build > Build Solution.
#
#   Using Ninja with Visual Studio 2019 command-line tools:
#     (this path may be different depending on your Visual Studio installation)
#     > "C:\Program Files (x86)\Microsoft Visual Studio 16.0\VC\bin\vcvars32.bat"
#     > cmake -G "Ninja" -DCMAKE_BUILD_TYPE=Debug ..
#     > ninja cefclient cefsimple
#
# To perform a Windows build using a 64-bit CEF binary distribution:
#   Using the Visual Studio 2019 IDE:
#     > cmake -G "Visual Studio 16 Win64" ..
#     Open build\cef.sln in Visual Studio and select Build > Build Solution.
#
#   Using Ninja with Visual Studio 2019 command-line tools:
#     (this path may be different depending on your Visual Studio installation)
#     > "C:\Program Files (x86)\Microsoft Visual Studio 16.0\VC\bin\amd64\vcvars64.bat"
#     > cmake -G "Ninja" -DCMAKE_BUILD_TYPE=Debug ..
#     > ninja cefclient cefsimple

#
# Global setup.
#

cmake_minimum_required(VERSION 3.14 FATAL_ERROR)

# Only generate Debug and Release configuration types.
set(CMAKE_CONFIGURATION_TYPES Debug Release)

# Project name.
project(fchost)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

# Use folders in the resulting project files.
set_property(GLOBAL PROPERTY OS_FOLDERS ON)

#
# CEF_ROOT setup.
# This variable must be set to locate the binary distribution.
#

# The binary distribution is in a separate directory from your
# project. Locate the binary distribution using the CEF_ROOT
# environment variable.
#
# Set the CEF_ROOT environment variable before executing CMake. For example:
#    > set CEF_ROOT=c:\path\to\cef_binary_3.2704.xxxx.gyyyyyyy_windows32
#
# set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_CURRENT_SOURCE_DIR}/cmake")

#
# Load the CEF configuration.
#

# try to find system installation
find_package(CEF QUIET)

if(NOT DEFINED _CEF_ROOT_EXPLICIT) # if that fails, try the standard way
  include("${CMAKE_CURRENT_LIST_DIR}/cmake/FindCEF.cmake")
endif()

#
# Define CEF-based targets.
#

# Include the libcef_dll_wrapper target.
# Comes from the libcef_dll/CMakeLists.txt file in the binary distribution
# directory.
add_subdirectory(${CEF_LIBCEF_DLL_WRAPPER_PATH} libcef_dll_wrapper)

# Include application targets.
# Comes from the <target>/CMakeLists.txt file in the current directory.
if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/fchost")
  add_subdirectory(fchost)
endif()

# Display configuration settings.
PRINT_CEF_CONFIG()


code:
# Copyright (c) 2016 The Chromium Embedded Framework Authors. All rights
# reserved. Use of this source code is governed by a BSD-style license that
# can be found in the LICENSE file.

#
# This file is the CEF CMake configuration entry point and should be loaded
# using `find_package(CEF REQUIRED)`. See the top-level CMakeLists.txt file
# included with the CEF binary distribution for usage information.
#

# Find the CEF binary distribution root directory.
set(_CEF_ROOT "")
if(CEF_ROOT AND IS_DIRECTORY "${CEF_ROOT}")
  set(_CEF_ROOT "${CEF_ROOT}")
  set(_CEF_ROOT_EXPLICIT 1)
else()
  set(_ENV_CEF_ROOT "")
  if(DEFINED ENV{CEF_ROOT})
    file(TO_CMAKE_PATH "$ENV{CEF_ROOT}" _ENV_CEF_ROOT)
  endif()
  if(_ENV_CEF_ROOT AND IS_DIRECTORY "${_ENV_CEF_ROOT}")
    set(_CEF_ROOT "${_ENV_CEF_ROOT}")
    set(_CEF_ROOT_EXPLICIT 1)
  endif()
  unset(_ENV_CEF_ROOT)
endif()

if(NOT DEFINED _CEF_ROOT_EXPLICIT)
  message(FATAL_ERROR "Must specify a CEF_ROOT value via CMake or environment variable.")
endif()

if(NOT IS_DIRECTORY "${_CEF_ROOT}/cmake")
  message(FATAL_ERROR "No CMake bootstrap found for CEF binary distribution at: ${CEF_ROOT}.")
endif()

# Execute additional cmake files from the CEF binary distribution.
set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${_CEF_ROOT}/cmake")
include("cef_variables")
include("cef_macros")


code:
@ECHO OFF
REM Quickly rebuild 64-bit VS2019 solution/project files from the CMake files.

cmake -G "Visual Studio 16" -A x64
pause


code:
# FCHost

FCHost is an alternative HTML renderer for playing Pregmod based on Chromium. It has multiple advantages compared to normal browsers:

* No browser cache limits, allowing arbitrarily large save files.
* Saves are stored in an easily accessible way on disk, allowing for easy manual editing.
* No lost saves due to accidentally cleared cookies.
* Can be noticeable faster.

A precompiled version for Windows is available [here](https://mega.nz/folder/vzxgwKwL#L4V4JEk1YfWcvC7EG76TMg)

## Initial setup

### HTML files

Place FC_pregmod.html next to the FCHost executable. On Windows, this is `fchost.exe`

### Art resources

Art resources work the same way as with other browser. For embedded art no actions are necessary, while for the others
the art resources have to be placed in a `resources` directory next to the HTML file.

Elohiem's interactive WebGL:

- fchost.exe
- resources/
  - webgl

Shokusku's rendered image pack

- fchost.exe
- resources/
  - dynamic
  - renders

If making changes while the game is open make sure to refresh or restart FCHost before trying the art styles.

## Keybinds

| Action    | Key Kombination  |
| --------- | ---------------- |
| Zoom In   | Ctrl + Plus      |
| Zoom Out  | Ctrl + Minus     |
| Dev Tools | Ctrl + Shift + J |

## Save file locations

On Windows saves are stored in `%User%\Documents\FreeCities_Pregmod\FCHostPersistentStorage\`

## Building FCHost

If you want to build FCHost yourself please refer to [FCHost/Building_FCHost.txt](FCHost/Building_FCHost.txt).


code:
# Copyright (c) 2014 The Chromium Embedded Framework Authors. All rights
# reserved. Use of this source code is governed by a BSD-style license that
# can be found in the LICENSE file.

#
# Source files.
#

# fchost sources.
set(FCHOST_SRCS
  fchost_app.cc
  fchost_app.h
  fchost_handler.cc
  fchost_handler.h
  fchost_storage.cc
  fchost_storage.h
  fchost_storage_js.cc
  fchost_storage_js.h
  utility.cc
  utility.h
  )
set(FCHOST_SRCS_LINUX
  fchost_linux.cc
  fchost_handler_linux.cc
  )
set(FCHOST_SRCS_MACOSX
  fchost_mac.mm
  fchost_handler_mac.mm
  )
set(FCHOST_SRCS_WINDOWS
  fchost.exe.manifest
  fchost.rc
  fchost_win.cc
  resource.h
  fchost_handler_win.cc
  )
APPEND_PLATFORM_SOURCES(FCHOST_SRCS)
source_group(fchost FILES ${FCHOST_SRCS})

set(FCHOST_RES_SRCS_WINDOWS
  res/fchost.ico
  )
APPEND_PLATFORM_SOURCES(FCHOST_RES_SRCS)
source_group(fchost\\\\res FILES ${FCHOST_RES_SRCS})

set(FCHOST_SRCS
  ${FCHOST_SRCS}
  ${FCHOST_RES_SRCS}
  )

# fchost helper sources.
set(FCHOST_HELPER_SRCS_MACOSX
  process_helper_mac.cc
  )
APPEND_PLATFORM_SOURCES(FCHOST_HELPER_SRCS)
source_group(fchost FILES ${FCHOST_HELPER_SRCS})

# fchost resources.
set(FCHOST_RESOURCES_MAC_SRCS_MACOSX
  mac/Info.plist
  mac/fchost.icns
  )
APPEND_PLATFORM_SOURCES(FCHOST_RESOURCES_MAC_SRCS)
source_group(fchost\\\\mac FILES ${FCHOST_RESOURCES_MAC_SRCS})

set(FCHOST_RESOURCES_MAC_ENGLISH_LPROJ_SRCS_MACOSX
  mac/English.lproj/InfoPlist.strings
  mac/English.lproj/MainMenu.xib
  )
APPEND_PLATFORM_SOURCES(FCHOST_RESOURCES_MAC_ENGLISH_LPROJ_SRCS)
source_group(fchost\\\\mac\\\\English.lproj FILES ${FCHOST_RESOURCES_MAC_ENGLISH_LPROJ_SRCS})

set(FCHOST_RESOURCES_SRCS
  ${FCHOST_RESOURCES_MAC_SRCS}
  ${FCHOST_RESOURCES_MAC_ENGLISH_LPROJ_SRCS}
  )

function (symlink_or_copy_to_binary_dir _target _srcFile)
  get_filename_component(_fileName "${_srcFile}" NAME)
  if (WIN32)
    set(action copy_if_different)
  else()
    set(action create_symlink)
  endif()
  add_custom_command(POST_BUILD
    TARGET ${_target}
    COMMAND ${CMAKE_COMMAND} -E ${action} "${_srcFile}" "$<TARGET_FILE_DIR:${_target}>/${_fileName}"
    )
endfunction()

#
# Shared configuration.
#

# Target executable names.
set(CEF_TARGET "fchost")
if(OS_MACOSX)
  set(CEF_HELPER_TARGET "fchost_Helper")
  set(CEF_HELPER_OUTPUT_NAME "fchost Helper")
else()
  # Logical target used to link the libcef library.
  ADD_LOGICAL_TARGET("libcef_lib" "${CEF_LIB_DEBUG}" "${CEF_LIB_RELEASE}")
endif()

# Determine the target output directory.
SET_CEF_TARGET_OUT_DIR()

# Executable target.
add_executable(${CEF_TARGET} MACOSX_BUNDLE WIN32 ${FCHOST_SRCS})

set(gameHTMLFile "FC_pregmod.html")
# Link the HTML where the executable expects to find it
symlink_or_copy_to_binary_dir(${CEF_TARGET} "${CMAKE_SOURCE_DIR}/../bin/${gameHTMLFile}")

#
# Linux configuration.
#

if(OS_LINUX)
  SET_EXECUTABLE_TARGET_PROPERTIES(${CEF_TARGET})
  add_dependencies(${CEF_TARGET} libcef_dll_wrapper)
  target_link_libraries(${CEF_TARGET} libcef_lib libcef_dll_wrapper ${CEF_STANDARD_LIBS})

  # Set rpath so that libraries can be placed next to the executable.
  set_target_properties(${CEF_TARGET} PROPERTIES INSTALL_RPATH "$ORIGIN")
  set_target_properties(${CEF_TARGET} PROPERTIES BUILD_WITH_INSTALL_RPATH TRUE)
  set_target_properties(${CEF_TARGET} PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${CEF_TARGET_OUT_DIR})

  # Copy binary files to the target output directory.
  #COPY_FILES("${CEF_TARGET}" "${CEF_BINARY_FILES}" "${CEF_BINARY_DIR}" "${CEF_TARGET_OUT_DIR}")
  foreach(cefBinaryFile IN LISTS CEF_BINARY_FILES)
  symlink_or_copy_to_binary_dir(${CEF_TARGET} "${CEF_BINARY_DIR}/${cefBinaryFile}")
  endforeach()
  COPY_FILES("${CEF_TARGET}" "${CEF_RESOURCE_FILES}" "${CEF_RESOURCE_DIR}" "${CEF_TARGET_OUT_DIR}")
  if (EXISTS "${CEF_BINARY_DIR}/libminigbm.so")
  symlink_or_copy_to_binary_dir(${CEF_TARGET} "${CEF_BINARY_DIR}/libminigbm.so")
  endif()
endif()

#
# Mac OS X configuration.
#

if(OS_MACOSX)
  option(OPTION_USE_ARC "Build with ARC (automatic Reference Counting) on macOS." ON)
  if(OPTION_USE_ARC)
    list(APPEND CEF_COMPILER_FLAGS
      -fobjc-arc
      )
    set_target_properties(${target} PROPERTIES
      CLANG_ENABLE_OBJC_ARC "YES"
      )
  endif()

  # Output paths for the app bundles.
  set(CEF_APP "${CEF_TARGET_OUT_DIR}/${CEF_TARGET}.app")
  set(CEF_HELPER_APP "${CEF_TARGET_OUT_DIR}/${CEF_HELPER_OUTPUT_NAME}.app")

  # Variable referenced from Info.plist files.
  set(PRODUCT_NAME "${CEF_TARGET}")

  # Helper executable target.
  add_executable(${CEF_HELPER_TARGET} MACOSX_BUNDLE ${FCHOST_HELPER_SRCS})
  SET_EXECUTABLE_TARGET_PROPERTIES(${CEF_HELPER_TARGET})
  add_dependencies(${CEF_HELPER_TARGET} libcef_dll_wrapper)
  target_link_libraries(${CEF_HELPER_TARGET} libcef_dll_wrapper ${CEF_STANDARD_LIBS})
  set_target_properties(${CEF_HELPER_TARGET} PROPERTIES
    MACOSX_BUNDLE_INFO_PLIST ${CMAKE_CURRENT_SOURCE_DIR}/mac/helper-Info.plist
    OUTPUT_NAME ${CEF_HELPER_OUTPUT_NAME}
    )

  # Main executable target.
  target_sources(${CEF_TARGET} ${FCHOST_RESOURCES_SRCS})
  SET_EXECUTABLE_TARGET_PROPERTIES(${CEF_TARGET})
  add_dependencies(${CEF_TARGET} libcef_dll_wrapper "${CEF_HELPER_TARGET}")
  target_link_libraries(${CEF_TARGET} libcef_dll_wrapper ${CEF_STANDARD_LIBS})
  set_target_properties(${CEF_TARGET} PROPERTIES
    MACOSX_BUNDLE_INFO_PLIST ${CMAKE_CURRENT_SOURCE_DIR}/mac/Info.plist
    )

  # Copy files into the main app bundle.
  add_custom_command(
    TARGET ${CEF_TARGET}
    POST_BUILD
    # Copy the helper app bundle into the Frameworks directory.
    COMMAND ${CMAKE_COMMAND} -E copy_directory
            "${CEF_HELPER_APP}"
            "${CEF_APP}/Contents/Frameworks/${CEF_HELPER_OUTPUT_NAME}.app"
    # Copy the CEF framework into the Frameworks directory.
    COMMAND ${CMAKE_COMMAND} -E copy_directory
            "${CEF_BINARY_DIR}/Chromium Embedded Framework.framework"
            "${CEF_APP}/Contents/Frameworks/Chromium Embedded Framework.framework"
    VERBATIM
    )

  # Manually process and copy over resource files.
  # The Xcode generator can support this via the set_target_properties RESOURCE
  # directive but that doesn't properly handle nested resource directories.
  # Remove these prefixes from input file paths.
  set(PREFIXES "mac/")
  COPY_MACOSX_RESOURCES("${FCHOST_RESOURCES_SRCS}" "${PREFIXES}" "${CEF_TARGET}" "${CMAKE_CURRENT_SOURCE_DIR}" "${CEF_APP}")
endif()

#
# Windows configuration.
#

if(OS_WINDOWS)
  # Executable target.
  add_dependencies(${CEF_TARGET} libcef_dll_wrapper)
  SET_EXECUTABLE_TARGET_PROPERTIES(${CEF_TARGET})
  target_link_libraries(${CEF_TARGET} libcef_lib libcef_dll_wrapper ${CEF_STANDARD_LIBS})

  # Add the custom manifest files to the executable.
  ADD_WINDOWS_MANIFEST("${CMAKE_CURRENT_SOURCE_DIR}" "${CEF_TARGET}" "exe")

  # Copy binary and resource files to the target output directory.
  COPY_FILES("${CEF_TARGET}" "${CEF_BINARY_FILES}" "${CEF_BINARY_DIR}" "${CEF_TARGET_OUT_DIR}")
  COPY_FILES("${CEF_TARGET}" "${CEF_RESOURCE_FILES}" "${CEF_RESOURCE_DIR}" "${CEF_TARGET_OUT_DIR}")
endif()


code:
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
  <compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
    <application>
      <!--The ID below indicates application support for Windows Vista -->
      <supportedOS Id="{e2011457-1546-43c5-a5fe-008deee3d3f0}"/>
      <!--The ID below indicates application support for Windows 7 -->
      <supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}"/>
      <!--The ID below indicates application support for Windows 8 -->
      <supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}"/>
      <!--The ID below indicates application support for Windows 8.1 -->
      <supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}"/>
      <!--The ID below indicates application support for Windows 10 -->
      <supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
    </application>
  </compatibility>
</assembly>


code:
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">

  <!--The compatibility section will be merged from build/win/compatibility.manifest -->

  <dependency>
    <dependentAssembly>
      <assemblyIdentity type="Win32" name="Microsoft.Windows.Common-Controls" version="6.0.0.0" processorArchitecture="*" publicKeyToken="6595b64144ccf1df" language="*"></assemblyIdentity>
    </dependentAssembly>
  </dependency>

  <trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
    <security>
      <requestedPrivileges>
        <requestedExecutionLevel level="asInvoker" />
      </requestedPrivileges>
    </security>
  </trustInfo>

</assembly>


code:
// Copyright (c) 2013 The Chromium Embedded Framework Authors. All rights
// reserved. Use of this source code is governed by a BSD-style license that
// can be found in the LICENSE file.

#include "fchost_app.h"

#include "./utility.h"

#include "include/cef_browser.h"
#include "include/cef_command_line.h"
#include "include/views/cef_browser_view.h"
#include "include/views/cef_window.h"
#include "include/wrapper/cef_helpers.h"
#include "fchost_handler.h"
#include "fchost_storage_js.h"

#include <filesystem>
#include <string>

#if defined(OS_LINUX)
  #include <unistd.h>
#endif

namespace {

// When using the Views framework this object provides the delegate
// implementation for the CefWindow that hosts the Views-based browser.
class SimpleWindowDelegate : public CefWindowDelegate {
 public:
  explicit SimpleWindowDelegate(CefRefPtr<CefBrowserView> browser_view)
      : browser_view_(browser_view) {}

  void OnWindowCreated(CefRefPtr<CefWindow> window) override {
    // Add the browser view and show the window.
    window->AddChildView(browser_view_);
    window->Show();

    // Give keyboard focus to the browser view.
    browser_view_->RequestFocus();
  }

  void OnWindowDestroyed(CefRefPtr<CefWindow> window) override {
    browser_view_ = nullptr;
  }

  bool CanClose(CefRefPtr<CefWindow> window) override {
    // Allow the window to close if the browser says it's OK.
    CefRefPtr<CefBrowser> browser = browser_view_->GetBrowser();
    if (browser)
      return browser->GetHost()->TryCloseBrowser();
    return true;
  }

  CefSize GetPreferredSize(CefRefPtr<CefView> view) override {
    return CefSize(800, 600);
  }

 private:
  CefRefPtr<CefBrowserView> browser_view_;

  IMPLEMENT_REFCOUNTING(SimpleWindowDelegate);
  DISALLOW_COPY_AND_ASSIGN(SimpleWindowDelegate);
};

std::filesystem::path executablePath() {
#if defined(OS_WIN)
  wchar_t target_path[_MAX_PATH];
  GetModuleFileNameW(NULL, target_path, _MAX_PATH);
  return std::filesystem::path(target_path);
#elif defined(OS_LINUX)
  std::string buf;
	buf.resize(32); // initial size estimate

	for (;; ) {
		ssize_t ret = readlink("/proc/self/exe", buf.data(), buf.size());

		if (ret == -1) {
			perror("getexename() failed");
			break;
		}

		if (static_cast<std::size_t>(ret) >= buf.size()) { // >= because we need the terminating NUL too
			buf.resize(buf.size() * 2);
			continue;
		}

		return buf;
	}

	return {};
#else
  #error "Platform-specific code required"
#endif
}
}  // namespace

FCHostApp::FCHostApp() {}

void FCHostApp::OnContextInitialized() {
  CEF_REQUIRE_UI_THREAD();

  CefRefPtr<CefCommandLine> command_line =
      CefCommandLine::GetGlobalCommandLine();

#if defined(OS_WIN) || defined(OS_LINUX)
  // Create the browser using the Views framework if "--use-views" is specified
  // via the command-line. Otherwise, create the browser using the native
  // platform framework. The Views framework is currently only supported on
  // Windows and Linux.
  const bool use_views = command_line->HasSwitch("use-views");
#else
  const bool use_views = false;
#endif

  // FCHostHandler implements browser-level callbacks.
  CefRefPtr<FCHostHandler> handler(new FCHostHandler(use_views));

  // Specify CEF browser settings here.
  CefBrowserSettings browser_settings;

  // Persist local storage
  browser_settings.local_storage = STATE_ENABLED;

  // For now, read from external file.  Probably want to make this a resource
  // at least on Windows.
  std::filesystem::path gameHTML = executablePath().parent_path() / "FC_pregmod.html";
#if defined(OS_WIN)
  std::wstring url = gameHTML.native();
#else
  std::string url = "file://" + gameHTML.string();
#endif

  if (use_views) {
    // Create the BrowserView.
    CefRefPtr<CefBrowserView> browser_view = CefBrowserView::CreateBrowserView(
        handler, url, browser_settings, nullptr, nullptr, nullptr);

    // Create the Window. It will show itself after creation.
    CefWindow::CreateTopLevelWindow(new SimpleWindowDelegate(browser_view));
  } else {
    // Information used when creating the native window.
    CefWindowInfo window_info;

#if defined(OS_WIN)
    // On Windows we need to specify certain flags that will be passed to
    // CreateWindowEx().
    window_info.SetAsPopup(NULL, "FCHost");
#endif

    // Create the first browser window.
    CefBrowserHost::CreateBrowser(window_info, handler, url, browser_settings,
                                  nullptr, nullptr);
  }
}

void FCHostApp::OnContextCreated(CefRefPtr<CefBrowser> browser, CefRefPtr<CefFrame> frame, CefRefPtr<CefV8Context> context) {
	FCHostStorageRegister(GetLocalStorePath() / "FCHostPersistentStorage", context->GetGlobal());
}

int FCHostApp::main(const CefMainArgs& args, CefCommandLine* commandLine)
{
  // Specify CEF global settings here.
  CefSettings settings;

  if (commandLine->HasSwitch("enable-chrome-runtime")) {
    // Enable experimental Chrome runtime. See issue #2969 for details.
    settings.chrome_runtime = true;
  }

  // Cache location is required for local storage
  cef_string_from_path(this->GetLocalStorePath(), &settings.cache_path);

  // When generating projects with CMake the CEF_USE_SANDBOX value will be defined
  // automatically. Pass -DUSE_SANDBOX=OFF to the CMake command-line to disable
  // use of the sandbox.
#if !defined(CEF_USE_SANDBOX)
    settings.no_sandbox = true;
#endif

  // Initialize CEF for the browser process.
  CefInitialize(args, settings, this, nullptr);

  // Run the CEF message loop. This will block until CefQuitMessageLoop() is
  // called.
  CefRunMessageLoop();

  // Shut down CEF.
  CefShutdown();

  return 0;
}


code:
// Copyright (c) 2013 The Chromium Embedded Framework Authors. All rights
// reserved. Use of this source code is governed by a BSD-style license that
// can be found in the LICENSE file.

#pragma once

#include "include/cef_app.h"

#include <filesystem>

class CefMainArgs;
class CefCommandLine;

// Implement application-level callbacks for the browser process.
class FCHostApp : public CefApp, public CefBrowserProcessHandler, public CefRenderProcessHandler {
 public:
  FCHostApp();

  // CefApp methods:
  virtual CefRefPtr<CefBrowserProcessHandler> GetBrowserProcessHandler()
      override {
    return this;
  }
  virtual CefRefPtr<CefRenderProcessHandler> GetRenderProcessHandler()
	  override {
	  return this;
  }

  // CefBrowserProcessHandler methods:
  virtual void OnContextInitialized() override;
  virtual void OnContextCreated(CefRefPtr<CefBrowser> browser, CefRefPtr<CefFrame> frame, CefRefPtr<CefV8Context> context) override;

  std::filesystem::path GetLocalStorePath();

  int main(const CefMainArgs& args, CefCommandLine* commandLine);

 private:
  // Include the default reference counting implementation.
  IMPLEMENT_REFCOUNTING(FCHostApp);
};


code:
// Copyright (c) 2013 The Chromium Embedded Framework Authors. All rights
// reserved. Use of this source code is governed by a BSD-style license that
// can be found in the LICENSE file.

#pragma once

#include "include/cef_client.h"

#include <list>

class FCHostHandler : public CefClient,
                      public CefDisplayHandler,
                      public CefLifeSpanHandler,
                      public CefLoadHandler,
					  public CefDownloadHandler,
					  public CefKeyboardHandler,
            private CefDialogHandler {
 public:
  explicit FCHostHandler(bool use_views);
  ~FCHostHandler();

  // Provide access to the single global instance of this object.
  static FCHostHandler* GetInstance();

  // CefClient methods:
  virtual CefRefPtr<CefDisplayHandler> GetDisplayHandler() override {
    return this;
  }
  virtual CefRefPtr<CefLifeSpanHandler> GetLifeSpanHandler() override {
    return this;
  }
  virtual CefRefPtr<CefLoadHandler> GetLoadHandler() override { return this; }
  virtual CefRefPtr<CefDownloadHandler> GetDownloadHandler() override { return this; }
  virtual CefRefPtr<CefKeyboardHandler> GetKeyboardHandler() override { return this; }

  CefRefPtr< CefDialogHandler > GetDialogHandler() override { return this; }
  bool OnFileDialog(CefRefPtr<CefBrowser> browser, CefDialogHandler::FileDialogMode mode,
    const CefString& title, const CefString& default_file_path, const std::vector<CefString>& accept_filters,
    int selected_accept_filter, CefRefPtr<CefFileDialogCallback> callback) override;

  // CefDisplayHandler methods:
  virtual void OnTitleChange(CefRefPtr<CefBrowser> browser,
                             const CefString& title) override;
  virtual bool OnConsoleMessage(CefRefPtr<CefBrowser> browser,
							    cef_log_severity_t level,
								const CefString& message,
								const CefString& source,
								int line) override;

  // CefLifeSpanHandler methods:
  virtual void OnAfterCreated(CefRefPtr<CefBrowser> browser) override;
  virtual bool DoClose(CefRefPtr<CefBrowser> browser) override;
  virtual void OnBeforeClose(CefRefPtr<CefBrowser> browser) override;

  // CefLoadHandler methods:
  virtual void OnLoadError(CefRefPtr<CefBrowser> browser,
                           CefRefPtr<CefFrame> frame,
                           ErrorCode errorCode,
                           const CefString& errorText,
                           const CefString& failedUrl) override;
  virtual void OnLoadEnd(CefRefPtr<CefBrowser> browser,
                         CefRefPtr<CefFrame> frame,
                         int httpStatusCode) override;

  // CefDownloadHandler methods:
  virtual void OnBeforeDownload(CefRefPtr<CefBrowser> browser,
								CefRefPtr<CefDownloadItem> download_item,
								const CefString& suggested_name,
								CefRefPtr< CefBeforeDownloadCallback > callback) override;

  // CefKeyboardHandler methods:
  virtual bool OnPreKeyEvent(CefRefPtr<CefBrowser> browser,
							const CefKeyEvent& event,
							CefEventHandle os_event,
							bool* is_keyboard_shortcut) override;

  // Request that all existing browser windows close.
  void CloseAllBrowsers(bool force_close);

  bool IsClosing() const { return is_closing_; }

 private:
  // Platform-specific implementation.
  void PlatformTitleChange(CefRefPtr<CefBrowser> browser,
                           const CefString& title);

  // True if the application is using the Views framework.
  const bool use_views_;

  // List of existing browser windows. Only accessed on the CEF UI thread.
  typedef std::list<CefRefPtr<CefBrowser>> BrowserList;
  BrowserList browser_list_;

  bool is_closing_;

  // Include the default reference counting implementation.
  IMPLEMENT_REFCOUNTING(FCHostHandler);
};


code:
// Copyright (c) 2014 The Chromium Embedded Framework Authors. All rights
// reserved. Use of this source code is governed by a BSD-style license that
// can be found in the LICENSE file.

#include "fchost_handler.h"

#if defined(CEF_X11)
#include <X11/Xatom.h>
#include <X11/Xlib.h>
#endif

#include "include/base/cef_logging.h"
#include "include/cef_browser.h"

#include <cstdlib>
#include <cstdio>
#include <filesystem>
#include <iomanip>
#include <memory>
#include <sstream>
#include <string>

// #define TRACE

#ifdef TRACE
	#include <iostream>
#endif

namespace {

	std::string executeProcessAndReadStdOut(const std::string& command)
	{
#ifdef TRACE
		std::cerr << "Command: " << command << std::endl;
#endif
		std::unique_ptr<FILE, decltype(&::pclose)> f(::popen(command.c_str(), "r"), &::pclose);
		std::ostringstream output;
		const int BUF_SIZE = 1024;
		char buf[BUF_SIZE];
		while(!std::feof(f.get())){
			std::size_t read = std::fread(buf, 1, BUF_SIZE, f.get());
			output.write(buf, static_cast<std::streamsize>(read));
		}
		std::string res = output.str();
		return res.substr(0, res.size() - 1); // to clear out CR
	}

	std::string runKDialog(CefDialogHandler::FileDialogMode mode, const CefString& title, const CefString& default_file_path,
		const std::vector<CefString>& accept_filters)
	{
		std::ostringstream cmdLine;
		cmdLine << "kdialog";
		if (!title.empty()) {
			cmdLine << " --title " <<  std::quoted(title.ToString());
		}

#ifdef TRACE
		std::cerr << "mode: " << std::hex << mode << std::dec << std::endl
			<< "default_file_path: " << default_file_path << std::endl
			<< "accept_filters: " << std::endl;

		for (const auto& f: accept_filters) {
			std::cerr << f.ToString() << std::endl;
		}
#endif

		// kdialog does not support these
		mode = static_cast<CefDialogHandler::FileDialogMode>(mode & ~(CefDialogHandler::FileDialogMode::FILE_DIALOG_OVERWRITEPROMPT_FLAG));
		mode = static_cast<CefDialogHandler::FileDialogMode>(mode & ~(CefDialogHandler::FileDialogMode::FILE_DIALOG_HIDEREADONLY_FLAG));

		switch (mode) {
			case CefDialogHandler::FileDialogMode::FILE_DIALOG_OPEN_MULTIPLE:
				cmdLine << " --multiple"; [[fallthrough]]
			case CefDialogHandler::FileDialogMode::FILE_DIALOG_OPEN:
				cmdLine << " --getopenfilename";
				break;
			case CefDialogHandler::FileDialogMode::FILE_DIALOG_SAVE:
				cmdLine << " --getsavefilename";
				break;
			case CefDialogHandler::FileDialogMode::FILE_DIALOG_OPEN_FOLDER:
				cmdLine << " --getexistingdirectory";
				break;
			default:
				break; // TODO
		}

		const std::string dsp = default_file_path.ToString();
		if (dsp.find('/') != std::string::npos) {
			cmdLine << ' ' << std::quoted(dsp);
		} else {
			// TODO save last used directory and put it here instead of $HOME
			cmdLine << ' ' << std::quoted(std::string(getenv("HOME")) + '/' + dsp);
		}

		if (accept_filters.size()) {
			cmdLine << " \'";
			for (const auto& f: accept_filters) {
				const auto s = f.ToString();
				cmdLine << " |" << s << " file(*" << s << ')';
			}
			cmdLine << '\'';
		}

		return executeProcessAndReadStdOut(cmdLine.str());
	}

	std::string runZenity(CefDialogHandler::FileDialogMode mode, const CefString& title, const CefString& default_file_path,
		const std::vector<CefString>& accept_filters)
	{
		std::ostringstream cmdLine;
		cmdLine << "zenity --file-selection";

		// zenity does not support these
		mode = static_cast<CefDialogHandler::FileDialogMode>(mode & ~(CefDialogHandler::FileDialogMode::FILE_DIALOG_HIDEREADONLY_FLAG));

		if (mode & CefDialogHandler::FileDialogMode::FILE_DIALOG_OVERWRITEPROMPT_FLAG) {
			cmdLine << " --confirm-overwrite";
		}
		mode = static_cast<CefDialogHandler::FileDialogMode>(mode & ~(CefDialogHandler::FileDialogMode::FILE_DIALOG_OVERWRITEPROMPT_FLAG));

		switch (mode) {
			case CefDialogHandler::FileDialogMode::FILE_DIALOG_OPEN_MULTIPLE:
				cmdLine << " --multiple";
				break;
			case CefDialogHandler::FileDialogMode::FILE_DIALOG_OPEN:
				// this is the implicit default mode for zenity
				break;
			case CefDialogHandler::FileDialogMode::FILE_DIALOG_SAVE:
				cmdLine << " --save";
				break;
			case CefDialogHandler::FileDialogMode::FILE_DIALOG_OPEN_FOLDER:
				cmdLine << " --directory";
				break;
			default:
				break; // TODO ?
		}

		const std::string dsp = default_file_path.ToString();
		if (dsp.find('/') != std::string::npos) {
			cmdLine << " --filename=" << std::quoted(dsp);
		} else {
			// TODO save last used directory and put it here instead of $HOME
			cmdLine << " --filename=" << std::quoted(std::string(getenv("HOME")) + '/' + dsp);
		}

		for (const auto& f: accept_filters) {
			const auto s = f.ToString();
			cmdLine << " --file-filter=\"" << s << " file | *" << s << '"';
		}

		return executeProcessAndReadStdOut(cmdLine.str());
	}

	using DialogRunFunc = std::string (*)(CefDialogHandler::FileDialogMode mode, const CefString& title, const CefString& default_file_path,
		const std::vector<CefString>& accept_filters);

	class DialogHelper {
		public:
			DialogHelper();
			std::string operator()(CefDialogHandler::FileDialogMode mode, const CefString& title, const CefString& default_file_path,
		const std::vector<CefString>& accept_filters) const
		{
			return func_ ? func_(mode, title, default_file_path, accept_filters) : "";
		}
		private:
			DialogRunFunc func_;
	};

	DialogHelper::DialogHelper()
	{
		// we will try to launch kdialog or zenity
		std::string dialogExecutable;
		// try to determine which environment we run inside
		std::string desktop = getenv("XDG_CURRENT_DESKTOP");

		const auto checkExeExists = [](const char* name) {
			int ec = ::system((std::string(name) + " --help > /dev/null").c_str());
			return ec >= 0 && ec < 127;
		};

		if (desktop == "KDE") {
			dialogExecutable = "kdialog";
		} else if (desktop == "GNOME") {
			dialogExecutable = "zenity";
		} else {
			// well, let's check executables
			if (checkExeExists("zenity")) {
				dialogExecutable = "zenity";
			} else if (checkExeExists("kdialog")){
				dialogExecutable = "kdialog";
			}
		}

#ifdef TRACE
		std::cerr << "dialogExecutable: " << dialogExecutable << std::endl
			<< "checkExeExists: " <<  checkExeExists(dialogExecutable.c_str()) << std::endl;
#endif

		if (!dialogExecutable.empty() && checkExeExists(dialogExecutable.c_str())) {
			if (dialogExecutable == "kdialog") {
				func_ = &runKDialog;
			} else if (dialogExecutable == "zenity") {
				func_ = &runZenity;
			} else {
				func_ = nullptr;
			}
		}
	}

}

void FCHostHandler::PlatformTitleChange(CefRefPtr<CefBrowser> browser, const CefString& title) {
	std::string titleStr(title);

#if defined(CEF_X11)
	// Retrieve the X11 display shared with Chromium.
	::Display* display = cef_get_xdisplay();
	DCHECK(display);

	// Retrieve the X11 window handle for the browser.
	::Window window = browser->GetHost()->GetWindowHandle();
	if (window == kNullWindowHandle)
		return;

	// Retrieve the atoms required by the below XChangeProperty call.
	const char* kAtoms[] = {"_NET_WM_NAME", "UTF8_STRING"};
	Atom atoms[2];
	int result =
			XInternAtoms(display, const_cast<char**>(kAtoms), 2, false, atoms);
	if (!result)
		NOTREACHED();

	// Set the window title.
	XChangeProperty(display, window, atoms[0], atoms[1], 8, PropModeReplace,
									reinterpret_cast<const unsigned char*>(titleStr.c_str()),
									titleStr.size());

	// TODO(erg): This is technically wrong. So XStoreName and friends expect
	// this in Host Portable Character Encoding instead of UTF-8, which I believe
	// is Compound Text. This shouldn't matter 90% of the time since this is the
	// fallback to the UTF8 property above.
	XStoreName(display, browser->GetHost()->GetWindowHandle(), titleStr.c_str());
#endif	// defined(CEF_X11)
}

bool FCHostHandler::OnFileDialog(CefRefPtr<CefBrowser> /* browser */, CefDialogHandler::FileDialogMode mode,
		const CefString& title, const CefString& default_file_path, const std::vector<CefString>& accept_filters,
		int /* selected_accept_filter */, CefRefPtr<CefFileDialogCallback> callback)
{
	static DialogHelper helper;

	std::string fn = helper(mode, title, default_file_path, accept_filters);
#ifdef TRACE
	std::cerr << "fn: " << fn << std::endl;
#endif
	if (fn.empty()) {
		callback->Cancel();
		return false;
	}

	std::vector<CefString> selected;
	selected.push_back(fn);
	callback->Continue(0, selected);
	return  true;
}


code:
// Copyright (c) 2013 The Chromium Embedded Framework Authors. All rights
// reserved. Use of this source code is governed by a BSD-style license that
// can be found in the LICENSE file.

#include "fchost_handler.h"

#include <windows.h>
#include <string>

#include "include/cef_browser.h"
#include "resource.h"

void FCHostHandler::PlatformTitleChange(CefRefPtr<CefBrowser> browser,
                                        const CefString& title) {
  CefWindowHandle hwnd = browser->GetHost()->GetWindowHandle();

  static bool haveSetIcon = false;
  if (!haveSetIcon)
  {
	  HICON hicon = LoadIcon(GetModuleHandle(NULL), MAKEINTRESOURCE(IDI_FCHOST));
	  SendMessage(hwnd, WM_SETICON, ICON_SMALL, reinterpret_cast<LPARAM>(hicon));
	  SendMessage(hwnd, WM_SETICON, ICON_BIG, reinterpret_cast<LPARAM>(hicon));
	  DestroyIcon(hicon);
	  haveSetIcon = true;
  }

  SetWindowText(hwnd, std::wstring(title).c_str());
}

bool FCHostHandler::OnFileDialog(CefRefPtr<CefBrowser> /* browser */, CefDialogHandler::FileDialogMode /* mode */,
    const CefString& /* title */, const CefString& /* default_file_path */, const std::vector<CefString>& /* accept_filters */,
    int /* selected_accept_filter */, CefRefPtr<CefFileDialogCallback> /* callback */)
{
  return false; // to display the default dialog
}


code:
// Copyright (c) 2013 The Chromium Embedded Framework Authors. All rights
// reserved. Use of this source code is governed by a BSD-style license that
// can be found in the LICENSE file.

#include "fchost_app.h"

#if defined(CEF_X11)
#include <X11/Xlib.h>
#endif

#include "include/base/cef_logging.h"
#include "include/cef_command_line.h"

#include <filesystem>

std::filesystem::path FCHostApp::GetLocalStorePath()
{
  return std::filesystem::path{getenv("HOME")} / ".local" / "share" / "FreeCities_Pregmod";
}

#if defined(CEF_X11)
namespace {

int XErrorHandlerImpl(Display* display, XErrorEvent* event) {
  LOG(WARNING) << "X error received: "
               << "type " << event->type << ", "
               << "serial " << event->serial << ", "
               << "error_code " << static_cast<int>(event->error_code) << ", "
               << "request_code " << static_cast<int>(event->request_code)
               << ", "
               << "minor_code " << static_cast<int>(event->minor_code);
  return 0;
}

int XIOErrorHandlerImpl(Display* display) {
  return 0;
}

}  // namespace
#endif  // defined(CEF_X11)

// Entry point function for all processes.
int main(int argc, char* argv[]) {
  // Provide CEF with command-line arguments.
  CefMainArgs main_args(argc, argv);

  // It will create the first browser instance in OnContextInitialized() after
  // CEF has initialized.
  CefRefPtr<FCHostApp> app(new FCHostApp);

  // CEF applications have multiple sub-processes (render, plugin, GPU, etc)
  // that share the same executable. This function checks the command-line and,
  // if this is a sub-process, executes the appropriate logic.
  int exit_code = CefExecuteProcess(main_args, app, nullptr);
  if (exit_code >= 0) {
    // The sub-process has completed so return here.
    return exit_code;
  }

#if defined(CEF_X11)
  // Install xlib error handlers so that the application won't be terminated
  // on non-fatal errors.
  XSetErrorHandler(XErrorHandlerImpl);
  XSetIOErrorHandler(XIOErrorHandlerImpl);
#endif

  CefRefPtr<CefCommandLine> command_line = CefCommandLine::CreateCommandLine();
  command_line->InitFromArgv(argc, argv);

  return app->main(main_args, command_line.get());
}


code:
#include "fchost_storage.h"

#include <algorithm>
#include <cstdio>
#include <memory>
#include <vector>

namespace fs = std::filesystem;

namespace {
	struct closeFile {
		void operator()(std::FILE* f) const noexcept
		{
			std::fclose(f);
		}
	};
}

CefRefPtr<CefV8Value> FCHostSessionStorage::keys() const
{
	// holy hell this is awful. I hope Sugarcube doesn't actually USE this...
	CefRefPtr<CefV8Value> ret = CefV8Value::CreateArray(static_cast<int>(storage.size()));
	auto itr = storage.cbegin();
	for (int i = 0; i < static_cast<int>(storage.size()); ++i, ++itr)
	{
		ret->SetValue(i, CefV8Value::CreateString(itr->first));
	}
	return ret;
}

/* This shouldn't happen, so don't waste time on it. Sugarcube really only writes simple alphanumeric keys.
static bool SanitizePath(std::wstring& inpath)
{
	std::transform(inpath.begin(), inpath.end(), inpath.begin(), [](wchar_t c)
	{
		if (PathGetCharTypeW(c) != GCT_LFNCHAR)
			return L'_';
		else
			return c;
	});
}
*/

FCHostPersistentStorage::FCHostPersistentStorage(const std::filesystem::path& _path)
	: path{_path}
{
	ensure_folder_exists();
	load();
}

void FCHostPersistentStorage::set(const CefString& key, CefRefPtr<CefV8Value> val)
{
	base::set(key, val);

	// only strings get persisted (should be OK, Sugarcube will serialize first)
	if (val->IsString())
	{
		// we should probably be doing this async but TBT Sugarcube is the slow part, not the file IO
		std::unique_ptr<std::FILE, closeFile> fh;
#if defined(OS_WIN)
		fh.reset(_wfopen(get_filename(key).c_str(), L"w"));
#else
		fh.reset(std::fopen(get_filename(key).c_str(), "w"));
#endif
		CefString valStr = val->GetStringValue();
		if (valStr.size() > 0) {
			std::fwrite(valStr.c_str(), static_cast<size_t>(valStr.size() * sizeof(valStr.c_str()[0])), 1, fh.get());
		}
	}
}

bool FCHostPersistentStorage::remove(const CefString& key)
{
	fs::remove(get_filename(key));
	return base::remove(key);
}

void FCHostPersistentStorage::clear()
{
	for (const auto& entry: fs::directory_iterator(path)) {
		if (fs::is_regular_file(entry.path())) {
			fs::remove_all(entry.path());
		}
	}
	base::clear();
}

void FCHostPersistentStorage::load()
{
	std::vector<char> readbuf;

	std::unique_ptr<std::FILE, closeFile> fh;
	for (const auto& entry: fs::directory_iterator(path)) {
		if (fs::is_regular_file(entry.path())) {
			const auto entrySize = fs::file_size(entry.path());
			readbuf.resize(static_cast<std::size_t>(entrySize + 2)); // +1 wchar_t

#if defined(OS_WIN)
			fh.reset(_wfopen(entry.path().c_str(), L"r"));
#else
			fh.reset(std::fopen(entry.path().c_str(), "r"));
#endif
			if (std::fread(&readbuf[0], entrySize, 1, fh.get())) {
				readbuf[entrySize + 1] = readbuf[entrySize] = 0; // null terminate
				CefString val = static_cast<const wchar_t*>(static_cast<const void*>(readbuf.data()));
				storage.emplace(entry.path().filename().native(), CefV8Value::CreateString(val));
			}
		}
	}
}

fs::path FCHostPersistentStorage::get_filename(const CefString& key) const
{
#if defined (OS_WIN)
	return path / key.ToWString();
#else
	return path / key.ToString();
#endif
}

void FCHostPersistentStorage::ensure_folder_exists() const
{
	// ignore returned errors
	fs::create_directories(path);
}


code:
#pragma once

#include "include/cef_app.h" // probably excessive

#include <filesystem>
#include <map>
#include <string>

// simple memory-backed session storage
class FCHostSessionStorage
{
public:
	virtual size_t size() const { return storage.size(); };
	virtual CefRefPtr<CefV8Value> /* string array */ keys() const;
	virtual bool has(const CefString& key) const { return (storage.find(key) != storage.cend()); };
	virtual CefRefPtr<CefV8Value> get(const CefString& key) const
	{
		auto itr = storage.find(key);
		if (itr == storage.cend())
			return CefV8Value::CreateNull();
		else
			return itr->second;
	};
	virtual void set(const CefString& key, CefRefPtr<CefV8Value> val) { storage.insert_or_assign(key, val); };
	virtual bool remove(const CefString& key) { return (storage.erase(key) > 0); };
	virtual void clear() { storage.clear(); };

protected:
	std::map<CefString, CefRefPtr<CefV8Value>> storage;
};

// memory-backed, disk-persistent local storage
class FCHostPersistentStorage : public FCHostSessionStorage
{
	using base = FCHostSessionStorage;
public:
	FCHostPersistentStorage(const std::filesystem::path& _path);

	virtual void set(const CefString& key, CefRefPtr<CefV8Value> val) override;
	virtual bool remove(const CefString& key) override;
	virtual void clear() override;

private:
	void load();
	void ensure_folder_exists() const;
	std::filesystem::path get_filename(const CefString& key) const;

	std::filesystem::path path;
};


code:
#include "fchost_storage_js.h"

#include <memory>

// static storage objects (not threadsafe, probably doesn't matter, since they'll execute within a JS context)

static std::unique_ptr<FCHostPersistentStorage> persist;
static std::unique_ptr<FCHostSessionStorage> session;

// storage handler js hook implementations

#define REGJSFUNC(x) { \
CefRefPtr<CefV8Value> func = CefV8Value::CreateFunction(x, handler); \
object->SetValue(x, func, V8_PROPERTY_ATTRIBUTE_NONE); }

static void AttachStorageFunctions(CefRefPtr<CefV8Value> object, CefRefPtr<CefV8Handler> handler)
{
	REGJSFUNC("size");
	REGJSFUNC("keys");
	REGJSFUNC("has");
	REGJSFUNC("get");
	REGJSFUNC("set");
	REGJSFUNC("remove");
	REGJSFUNC("clear");
}

void FCHostStorageRegister(const std::filesystem::path& persistPath, CefRefPtr<CefV8Value> object) {
	// New context, reset everything (don't try to run more than one app at once!)
	if (persist) persist.reset(NULL);
	if (session) session.reset(NULL);

	persist = std::make_unique<FCHostPersistentStorage>(persistPath);
	session = std::make_unique<FCHostSessionStorage>();

	CefRefPtr<CefV8Handler> sess_handler = new FCHostStorageHandler(false);
	CefRefPtr<CefV8Value> obj_sess = CefV8Value::CreateObject(nullptr, nullptr);
	object->SetValue("FCHostSession", obj_sess, V8_PROPERTY_ATTRIBUTE_NONE);
	AttachStorageFunctions(obj_sess, sess_handler);

	CefRefPtr<CefV8Handler> pers_handler = new FCHostStorageHandler(true);
	CefRefPtr<CefV8Value> obj_pers = CefV8Value::CreateObject(nullptr, nullptr);
	object->SetValue("FCHostPersistent", obj_pers, V8_PROPERTY_ATTRIBUTE_NONE);
	AttachStorageFunctions(obj_pers, pers_handler);
}

// individual execution objects for session/persistent storage

bool FCHostStorageHandler::Execute(const CefString& name, CefRefPtr<CefV8Value> object, const CefV8ValueList& arguments, CefRefPtr<CefV8Value>& retval, CefString& exception)
{
	// alias pointer only, no ownership
	FCHostSessionStorage* storage = persistent ? persist.get() : session.get();

	if (name == "size") {
		// no arguments
		retval = CefV8Value::CreateInt(static_cast<int32>(storage->size()));
		return true;
	}
	else if (name == "keys") {
		// no arguments
		retval = storage->keys();
		return true;
	}
	else if (name == "has") {
		// one string argument
		if (arguments.size() < 1 || !arguments[0]->IsString()) return false;

		retval = CefV8Value::CreateBool(storage->has(arguments[0]->GetStringValue()));
		return true;
	}
	else if (name == "get") {
		// one string argument
		if (arguments.size() < 1 || !arguments[0]->IsString()) return false;

		retval = storage->get(arguments[0]->GetStringValue());
		return true;
	}
	else if (name == "set") {
		// two arguments - one string, one "whatever"
		if (arguments.size() < 2 || !arguments[0]->IsString()) return false;

		storage->set(arguments[0]->GetStringValue(), arguments[1]);
		retval = CefV8Value::CreateBool(true);
		return true;
	}
	else if (name == "remove") {
		// one string argument
		if (arguments.size() < 1 || !arguments[0]->IsString()) return false;

		retval = CefV8Value::CreateBool(storage->remove(arguments[0]->GetStringValue()));
		return true;
	}
	else if (name == "clear") {
		// no arguments
		storage->clear();
		return true;
	}

	return false;
}


code:
#pragma once

#include "fchost_storage.h"

// storage handlers that can be registered directly to Javascript to enable fchost_storage backing

class FCHostStorageHandler : public CefV8Handler
{
public:
	FCHostStorageHandler(bool _persistent) : persistent(_persistent) {};

	virtual bool Execute(const CefString& name,
		CefRefPtr<CefV8Value> object,
		const CefV8ValueList& arguments,
		CefRefPtr<CefV8Value>& retval,
		CefString& exception) override;

private:
	bool persistent;
	IMPLEMENT_REFCOUNTING(FCHostStorageHandler);
};

void FCHostStorageRegister(const std::filesystem::path& persistPath, CefRefPtr<CefV8Value> object);


code:
// Copyright (c) 2013 The Chromium Embedded Framework Authors. All rights
// reserved. Use of this source code is governed by a BSD-style license that
// can be found in the LICENSE file.

#include <windows.h>
#include <KnownFolders.h>
#include <ShlObj.h>

#include "fchost_app.h"

std::filesystem::path FCHostApp::GetLocalStorePath()
{
	PWSTR ppath;
	SHGetKnownFolderPath(FOLDERID_Documents, 0, NULL, &ppath);
	std::filesystem::path local_storage = ppath;
	CoTaskMemFree(ppath);
	return local_storage / L"FreeCities_Pregmod";
}

// Entry point function for all processes.
int APIENTRY wWinMain(HINSTANCE hInstance,
                      HINSTANCE hPrevInstance,
                      LPTSTR lpCmdLine,
                      int nCmdShow) {
  UNREFERENCED_PARAMETER(hPrevInstance);
  UNREFERENCED_PARAMETER(lpCmdLine);

  // Enable High-DPI support on Windows 7 or newer.
  CefEnableHighDPISupport();

  // Provide CEF with command-line arguments.
  CefMainArgs main_args(hInstance);

  // It will create the first browser instance in OnContextInitialized() after
  // CEF has initialized.
  CefRefPtr<FCHostApp> app(new FCHostApp);

  // CEF applications have multiple sub-processes (render, plugin, GPU, etc)
  // that share the same executable. This function checks the command-line and,
  // if this is a sub-process, executes the appropriate logic.
  int exit_code = CefExecuteProcess(main_args, app, nullptr);
  if (exit_code >= 0) {
    // The sub-process has completed so return here.
    return exit_code;
  }

  CefRefPtr<CefCommandLine> command_line = CefCommandLine::CreateCommandLine();
  command_line->InitFromString(lpCmdLine);

  return app->main(main_args, command_line.get());
}


code:
// Copyright (c) 2013 The Chromium Embedded Framework Authors. All rights
// reserved. Use of this source code is governed by a BSD-style license that
// can be found in the LICENSE file.

//{{NO_DEPENDENCIES}}
// Microsoft Visual C++ generated include file.
// Used by fchost.rc
//

#define IDI_FCHOST 100

// Avoid files associated with MacOS
#define _X86_

// Next default values for new objects
//
#ifdef APSTUDIO_INVOKED
#ifndef APSTUDIO_READONLY_SYMBOLS
#define _APS_NO_MFC 1
#define _APS_NEXT_RESOURCE_VALUE 102
#define _APS_NEXT_COMMAND_VALUE 32700
#define _APS_NEXT_CONTROL_VALUE 1000
#define _APS_NEXT_SYMED_VALUE 102
#endif
#endif


code:
#include "./utility.h"

CefString cef_string_from_path(const std::filesystem::path& p)
{
	return CefString(p.native());
}

void cef_string_from_path(const std::filesystem::path& p, cef_string_t* str)
{
	const auto& pstr = p.native();
#if defined(OS_WIN)
	cef_string_from_utf16(pstr.c_str(), pstr.size(), str);
#else
	cef_string_from_utf8(pstr.c_str(), pstr.size(), str);
#endif
}


code:
#pragma once

#include "include/capi/cef_base_capi.h"

#include <filesystem>

CefString cef_string_from_path(const std::filesystem::path& p);
void cef_string_from_path(const std::filesystem::path& p, cef_string_t* str);


code:
MAKEFLAGS += -r
export TWEEGO_PATH=devTools/tweeGo/storyFormats
HASH := $(shell git rev-list -n 1 --abbrev-commit HEAD)
COMMIT := $(shell git rev-parse --short HEAD)
TWEEGO := $(shell command -v tweego 2>/dev/null)
ifndef TWEEGO
	uname := $(shell uname -s)
	arch := $(shell uname -m)
	arch := $(shell if test $(arch) = x86_64 -o $(arch) = amd64; then echo 64; else echo 32; fi)
	ifeq ($(uname),Linux)
		ifeq ($(arch),64)
			TWEEGO := devTools/tweeGo/tweego_nix64
		else
			TWEEGO := devTools/tweeGo/tweego_nix86
		endif
	else ifeq ($(uname),Darwin)
		ifeq ($(arch),64)
			TWEEGO := devTools/tweeGo/tweego_osx64
		else
			TWEEGO := devTools/tweeGo/tweego_osx86
		endif
	endif
endif

all: bin/FC_pregmod.html bin/resources

git: bin/FC_pregmod_$(HASH).html bin/resources

bin/resources: resources
	test -L "$@" || ln -s "../$<" bin/

bin/%.html: bin/tmp
	rm src/002-config/fc-version.js.commitHash.js
	mv $< $@

# concatenated module files
bin/fc.%: bin/
	devTools/concatFiles.sh $*/ '*.$*' $@

bin/tmp: bin/fc.js bin/fc.css injectGitCommitHash
	$(TWEEGO) --module=bin/fc.js --module=bin/fc.css --head resources/raster/favicon/arcologyVector.html src/ > $@
	rm -f bin/fc.js
	rm -f bin/fc.css

injectGitCommitHash:
	printf "App.Version.commitHash = '%s';\n" $(COMMIT) > src/002-config/fc-version.js.commitHash.js

bin/:
	mkdir -p $@

sanity:
	./sanityCheck.sh

sugarcube:
	(cd submodules/sugarcube-2/ && node build.js -n -b 2)
	mv submodules/sugarcube-2/build/twine2/sugarcube-2/format.js devTools/tweeGo/storyFormats/sugarcube-2/format.js
	(cd submodules/sugarcube-2/ && git diff master fc > sugarcube-fc-changes.patch)
	mv submodules/sugarcube-2/sugarcube-fc-changes.patch devNotes/"sugarcube stuff"/sugarcube-fc-changes.patch

.PHONY: all sanity git injectGitCommitHash sugarcube

path: Makefile


code:
# Free Cities - pregmod

Pregmod is a modification of the original [Free Cities](https://freecitiesblog.blogspot.com/) created by FCdev.

## Play the game

You can download compiled files and source archives from the [Releases page](https://gitgud.io/pregmodfan/fc-pregmod/-/releases), and the [build](https://gitgud.io/pregmodfan/fc-pregmod/-/jobs/artifacts/pregmod-master/download?job=build) from the latest commit to the master branch.

Alternatively, you can build the game yourself:

First, clone the git repository:

1. [Install Git for terminal](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) or a Git GUI of your choice.
2. Clone the repo
    * Via terminal: `git clone --single-branch https://gitgud.io/pregmodfan/fc-pregmod.git`
3. Get updates
    * Via terminal: `git pull`

Compile the game:

* Windows
    * Run compile.bat
    * Second run of compile.bat will overwrite without prompt

* Linux/Mac
    1. Ensure executable permission on file `devTools/tweeGo/tweego` (not tweego.exe!)
    2. Ensure executable permission on file `compile.sh`
    3. In the root dir of sources (where you see src, devTools, bin...) run command `./compile.sh` from console.
       Alternatively, if you have make installed, run `make all` in the root directory.

To play open FC_pregmod.html in bin/ (Recommendation: Drag it into incognito mode)

## Common problems

* If compiling takes a while or causes a noticeable increase in system resource utilisation.
	- It might be worth checking your main Antivirus (AV) settings.
	- If it is Windows Defender (currently tested with Windows 10): Start menu -> Windows Security -> Virus & threat protection -> Virus & threat protection settings -> Manage settings -> Exclusions (near the bottom) -> Add or remove exclusions -> Add an exclusion -> path to bin/.

* `sessionStorage quota exceeded` / `localStorage quota exceeded` or something similar
    - Your saves stored inside the browser are getting too large. There are multiple ways to solve this:
        1. Delete saves stored in the browser. If you want to keep them, save them to disk first.
        2. Disable autosave and delete the current one. Due to technical reasons autosaves are larger than normal saves,
           so this may help more than expected.
        3. If on Firefox, raise the storage limit: Type `about:config` in the address bar and search for
           `dom.storage.default_quota`. Increase this value as needed. Default value is 5120 kilobytes / 5 MB.
        4. Switch to a different browser. Recommended is either Firefox or FCHost, a custom HTML renderer specifically
           for Pregmod. Further reading can be found [here](FCHost/documentation_FCHost.md).
        5. If you absolutely need to use Google Chrome:
            1. download and unzip [NW.js SDK](https://nwjs.io/downloads/) for your operative system.
            2. copy the game file (FC_pregmod.html) into the `nwjs-sdk-v0.XX.Y-YOUR_OS` folder
            3. in the same folder, create a text file with the following content:
               ```
               {
                   "name": "Free Cities pregmod edition",
                   "main": "FC_pregmod.html",
                   "dom_storage_quota":30
               }
               ```
               and save it as package.json. In this example, 30 is the limit (in MB) that is set for the storage quota,
               but you can replace it with any number. Google Chrome has the same default value as Firefox.
            4. Double click nw.exe to launch the game.

* Everything is broken!
    - **Do not copy over your existing download** as it may leave old files behind, replace it entirely

* I can't save more than once or twice.
    - Known issue caused by SugarCube level changes. Save to file doesn't have this problem and will likely avoid the first problem as well.
    - It is possible to increase the memory utilized by your browser to delay this

* I wish to report a sanityCheck issue.
    - Great, however a large majority of the results are false positives. That said, if you found an actual error it could be a great first contribution if you are interested.

## Contribute

New Contributors are always welcome. Basic information before you start can be found [here](CONTRIBUTING.md)

## Submodules

FC uses a modified version of SugarCube 2. More information can be found [here](devNotes/sugarcube stuff/building SugarCube.md).


code:
# How to vector art

Please read the whole document before starting to work.
Some aspects are not obvious right away.

Note: This does not actually describe how to be an artist.

## TL;DR

	killall inkscape
	artTools/vector_layer_split.py artTools/vector_source.svg tw src/art/vector/layers/
	compile
	python3 artTools/normalize_svg.py artTools/vector_source.svg
	git commit -a

## 1. Be an artist

Make changes to the vector_source.svg.
Inkscape was thoroughly tested.
Adobe Illustrator might work decently, too.

## 2. Respect the structure

While editing, keep the Layers in mind.

* In Inkscape, Layers are special "Inkscape groups".
* In Illustrator, Layers are groups with user-defined IDs.
* All Layers should have an ID that is globally unique
  (not just within their subtree).

* Please use anonymous groups only for the ease of editing. Remove all "helper" groups before finally saving the file.
* Anonymous groups can be used for continuous scaling (of e.g. boobs).

* Every asset that should go into a separate file needs to be in a labelled group
  (even if that is a group with only one shape).
* There are some globally available styles defined as CSS classes (e.g. skin, hair).
  Use them if your asset should be changed upon display.
  Do not set the style directly in the object.

## 3. Normalize the document (before committing)

**THIS IS IMPORTANT**

The various editors out there (Inkscape, Illustrator) may use different variants of formatting the SVG XML.
Use

	python3 normalize_svg.py vector_source.svg

before committing to normalize the format so git will not freak out about the changed indentation.

If you use Inkscape, please use in Edit → Settings → Input/Output → SVG-Output → Path Data → Optimized. This is the default.
In case your Editor uses another path data style which cannot be changed, please contact the other artists and developers via the issue tracker to find a new common ground.

What it does:
* Formats the SVG XML according to Pythons lxml module, regardless of editor.
* Adobe Illustrator uses group IDs as layer labels.
  Inkscape however uses layer labels and a separate, auto-generated group ID.
  normalize_svg.py overwrites the group ID with the Inkscape layer label
  so they are synchronized with Inkscape layer labels.
* Inkscape copies the global style definition into the object *every time*
  the object is edited. If an object references a CSS class AND at the same time
  has a local style, the local style is removed
  so global dynamic styling is possible later on.

Note: Behavior of Adobe Illustrator is untested.

## 4. Split the layers

For NoX original, deepmurk extensions, execute

	python3 vector_layer_split.py vector_deepmurk_primary.svg tw ../src/art/vector/layers/

For faraen revamped art (based on NoX original)

	python3 vector_revamp_layer_split.py vector_revamp_source.svg tw ../src/art/vector_revamp/layers/

. This application reads all groups in `vector_source.svg`.
Each group is stored in a separate file in the target directory `/src/art/vector/layers/`.
The group ID sets the file name. Therefore, the group ID **must not** contain spaces or any other weird characters.

Also consider:
* A group with ID ending in _ is ignored. Use Layers ending in _ to keep overview.
* A group with ID starting with "g" (Inkscape) or "XMLID" (Illustrator) is also ignored.
* Existing files are overwritten **without enquiry**.
* The target directory is not emptied. If a file is no longer needed, you should remove it manually.
* This procedure removes global definitions. This means, SVG filters are currently not supported.

Available output formats are `svg` and `tw`.
`svg` output exists for debug reasons.
`tw` embeds the SVG data into Twine files, but removes the global style definitions so they can be set during display.

## 5. Edit the code

`/src/art/` contains Twine code which shows the assets in the story.
There are many helpful comments in `/src/art/artWidgets.tw`.
The code also generates the previously removed global style definitions on the fly and per display.

## 6. Compile the story

Use the provided compile script (Windows batch or shell) for compiling.

## 7. Advanced usage

You can define multiple CSS classes to one element, e.g. "skin torso". When procedurally generating CSS, you can then set a global "skin" style, and a different one for "torso".

You can put variable text into the image using single quotes, e.g. "'+_tattooText+'". The single quotes *break* the quoting in Twine so the content of the Twine variable `_tattooText` is shown upon display. You can even align text on paths.

An anonymous group can be used for dynamic transformation. However, finding the correct parameters for the affine transformation matrix can be cumbersome. Use any method you see fit. See `src/art/vector/Boob.tw` for one example of the result.


code:
Apron
AttractiveLingerie
AttractiveLingerieForAPregnantWoman
BallGown
Battledress
Blouse
BodyOil
Bunny
Chains
ChattelHabit
Cheerleader
ClubslutNetting
ComfortableBodysuit
Conservative
Cutoffs
Cybersuit
FallenNunsHabit
FuckdollSuit
HalterTopDress
HaremGauze
Hijab
Huipil
Kimono
LatexCatsuit
Leotard
LongQipao
MaternityDress
MilitaryUniform
MiniDress
Monokini
NiceBusinessAttire
NiceMaid
NiceNurse
No
PenitentNunsHabit
RedArmyUniform
RestrictiveLatex
ScalemailBikini
SchoolgirlUniform
SchutzstaffelUniform
ShibariRopes
SlaveGown
Slutty
SluttyBusinessAttire
SluttyJewelry
SluttyMaid
SluttyNurse
SluttyQipao
SluttySchutzstaffelUniform
Spats
StretchPants
StringBikini
Succubus
Toga
UncomfortableStraps
Western


code:
#!/bin/bash

# This script reads all possible values of $slave.clothes as mentioned by the documentation.
# This script uses the actual implementation of the JS clothing2artSuffix function as defined in src/art/vector/Helper_Functions.tw
# This script outputs suffixes to be used in the SVG naming scheme
# This script is meant to be executed at the project root directory.
# This script depends on bash, grep, sed, paste and nodejs (so best executed on Linux, I guess)

# grep -Porh '(?<=\.clothes = )"[^"]+"' src/ | sort | uniq # searches sources for all clothes strings actually used in the game, even undocumented ones

(
  echo 'var window = {};'
  grep -v '^:' src/art/vector/Helper_Functions.tw
  echo -n 'Array('
  sed '/^clothes:/,/:/!d' "slave variables documentation - Pregmod.txt" | grep '"' | paste -sd,
  echo ').forEach(v => {console.log(window.clothing2artSuffix(v));});'
) | nodejs | sort


code:
#!/usr/bin/env python3

"""
Application for "normalizing" SVGs

These problems are addressed:

* Inkscape notoriously copies class styles into the object definitions.
https://bugs.launchpad.net/inkscape/+bug/167937

* Inkscape uses labels on layers. Layers are basically named groups.
  Inkscape does not sync the group id with the layer label.

Usage Example:
python3 inkscape_svg_fixup.py vector_source.svg
"""

import re
import sys

import lxml.etree as etree

color_classes = {
    'skin', 'head', 'torso', 'boob', 'penis', 'scrotum', 'belly', 'areola', 'bellybutton', 'labia', 'hair',
    'pubic_hair', 'armpit_hair', 'eyebrow_hair', 'shoe', 'shoe_shadow', 'smart_piercing', 'steel_piercing',
    'steel_chastity', 'outfit_base', 'gag', 'shadow', 'glasses', 'eye', 'sclera',
    'white', 'skin', 'skin_highlight', 'skin_shade', 'skin_strong_highlight', 'skin_strong_shade', 'arm',
    'arm_highlight', 'arm_shade', 'head', 'head_highlight', 'head_shade', 'torso', 'torso_highlight',
    'torso_shade', 'boob', 'boob_highlight', 'boob_shade', 'penis', 'penis_highlight', 'penis_shade',
    'scrotum', 'scrotum_highlight', 'scrotum_shade', 'belly', 'belly_highlight', 'belly_shade', 'neck',
    'neck_highlight', 'neck_shade', 'legs', 'legs_highlight', 'legs_shade', 'butt', 'butt_highlight',
    'butt_shade', 'feet', 'feet_highlight', 'feet_shade', 'areola', 'labia', 'hair', 'shoe_shadow',
    'smart_piercing', 'steel_piercing', 'steel_chastity', 'gag', 'shadow', 'glasses', 'lips', 'eyeball',
    'iris', 'highlight1', 'highlight2', 'highlight3', 'highlightStrong', 'armpit_hair', 'pubic_hair',
    'muscle_tone', 'belly_details', 'shoe_primary', 'shoe_primary_highlight', 'shoe_primary_shade',
    'shoe_accent', 'shoe_accent_highlight', 'shoe_accent_shade', 'top_primary', 'top_primary_highlight',
    'top_primary_shade', 'top_accent', 'top_accent_highlight', 'top_accent_shade', 'bottoms_primary',
    'bottoms_primary_highlight', 'bottoms_primary_shade', 'bottoms_accent', 'bottoms_accent_highlight',
    'bottoms_accent_shade', 'bra_primary', 'bra_primary_highlight', 'bra_primary_shade', 'bra_accent',
    'bra_accent_highlight', 'bra_accent_shade', 'bra_strap1', 'bra_strap2', 'bra_strap3', 'shirt_center1',
    'shirt_center2', 'shirt_center3', 'panties_primary', 'panties_primary_highlight', 'panties_primary_shade',
    'panties_accent', 'panties_accent_highlight', 'panties_accent_shade', 'stockings_primary',
    'stockings_accent', 'top_primary_strong_highlight', 'top_primary_strong_shade',
    'top_accent_strong_highlight', 'top_accent_strong_shade', 'bottoms_primary_strong_highlight',
    'bottoms_primary_strong_shade', 'bottoms_accent_strong_highlight', 'bottoms_accent_strong_shade',
    'bellymask_1', 'bellymask_2', 'bellymask_3', 'bellymask_4', 'bellymask_5', 'bellymask_6', 'bellymask_7',
    'bellymask_8', 'bellymask_9', 'bellymask_normal', 'bellymask_hourglass', 'bellymask_unnatural', "feet_nails"
}

def fix(tree):
    # know namespaces
    ns = {
        'svg': 'http://www.w3.org/2000/svg',
        'inkscape': 'http://www.inkscape.org/namespaces/inkscape'
    }

    # find document global style definition
    # mangle it and interpret as python dictionary
    style_element = tree.find('./svg:style', namespaces=ns)
    style_definitions = style_element.text
    pythonic_style_definitions = '{' + style_definitions. \
        replace('\t.', '"').replace('{', '":"').replace('}', '",'). \
        replace('/*', '#') + '}'
    styles = eval(pythonic_style_definitions)

    # go through all SVG elements
    for elem in tree.iter():
        if elem.tag == etree.QName(ns['svg'], 'g'):
            # compare inkscape label with group element ID
            lbl = elem.get(etree.QName(ns['inkscape'], 'label'))
            if lbl:
                i = elem.get('id')
                if i != lbl:
                    print("Overwriting ID %s with Label %s..." % (i, lbl))
                    elem.set('id', lbl)
        # clean styles (for easier manual merging)
        style_string = elem.get('style')
        if style_string:
            split_styles = style_string.strip('; ').split(';')
            styles_pairs = [s.strip('; ').split(':') for s in split_styles]
            filtered_pairs = [(k, v) for k, v in styles_pairs if not (
                    k.startswith('font-') or
                    k.startswith('text-') or
                    k.endswith('-spacing') or
                    k in ["line-height", " direction", " writing", " baseline-shift", " white-space", " writing-mode"]
            )]
            split_styles = [':'.join(p) for p in filtered_pairs]
            style_string = ';'.join(sorted(split_styles))
            elem.attrib["style"] = style_string

        # remove all style attributes offending class styles
        s = elem.get('style')
        c = elem.get('class')
        if c and s:
            if 'i' in locals():
                s = s.lower()
                c = c.split(' ')[0]  # regard main style only
                classes = c.split(' ')
                has_color_class = any(x in color_classes for x in classes)
                if has_color_class:
                    s_new = re.sub('fill:#[0-9a-f]+;?', '', s)
                    if s != s_new:
                        print("Explicit fill was removed from style string ({0}) for element with ID {1} "
                              "because its class ({2}) controls the fill color".format(s, i, c))
                        s = s_new
                if s == 'style=""':  # the style is empty now
                    del elem.attrib["style"]
                    continue
                cs = ''
                if c in styles:
                    cs = styles[c].strip('; ').lower()
                if c not in styles:
                    print("Object id %s references unknown style class %s." % (i, c))
                else:
                    if cs != s.strip('; '):
                        print("Style %s removed from object id %s differed from class %s style %s." % (s, i, c, cs))
                        del elem.attrib["style"]
            else:
                print('--------------------------- i not defined - propably a clip-path with a style ---------------------------')
                print("Element tag: ".format(elem.tag))
                print("Element style: ".format(s))
                print("Element class: ".format(c))
                print(etree.tostring(elem, pretty_print=True))
                print('--------------------------- i not defined - propably a clip-path with a style ---------------------------')
    # remove explicit fill color if element class is one of the color_classes

if __name__ == "__main__":
    input_file = sys.argv[1]
    tree = etree.parse(input_file)
    fix(tree)
    # store SVG into file (input file is overwritten)
    svg = etree.tostring(tree, pretty_print=True)
    with open(input_file, 'wb') as f:
        f.write('<?xml version="1.0" encoding="UTF-8" standalone="no"?>\n'.encode("utf-8"))
        f.write(svg)


code:
import os
import subprocess
import sys

if os.name == "nt":
    minifier = "../devTools/minify/minify_win_amd64.exe"
elif sys.platform == "darwin":
    minifier = "../devTools/minify/minify_darwin_amd64"
else:
    # probably linux, if not we don't have an alternative anyways.
    minifier = "../devTools/minify/minify_linux_amd64"

def save(layer, prefix, output_directory, output_format, svg_data):
    i = layer.get('id')
    output_path = os.path.join(output_directory, "{0}{1}.svg".format(prefix, i))
    if output_format == 'svg':
        with open(output_path, 'wb') as f:
            # Header for normal SVG (XML)
            f.write(f'<?xml version="1.0" encoding="UTF-8" standalone="no"?>{os.linesep}'.encode("utf-8"))
            f.write(svg_data)
    elif output_format == 'tw':
        # send to minifier through stdin, which then saves to file
        subprocess.run([minifier, "--mime=image/svg+xml", "-o", output_path],
                       input=svg_data)


code:
#!/usr/bin/env python3

"""
Application for procedural content adaption
*THIS IS VERY EXPERIMENTAL*
Contains a very poor man's implementation of spline mesh warping.

This application parses SVG path data of an outfit sample aligned to a body part.
The outfit is then replicated for other shapes of the same body part.

Example data is geared towards generating a strap outfit for boobs and torso
for all sizes of boobs and all shapes of torsos based on a single outfit for
boobs of size 2 and a hourglass torso respectively.

Limitations:
* handles paths only
* only svg.path Line and CubicBezier are tested
* only the aforementioned examples are tested

Usage:
python3 vector_clothing_replicator.py infile clothing bodypart destinationfile

Usage Example:
python3 vector_clothing_replicator.py vector_source.svg Straps Boob vector_destination.svg
python3 vector_clothing_replicator.py vector_source.svg Straps Torso vector_destination.svg
"""

import copy
import sys

import lxml.etree as etree
from svg.path import parse_path

REFERENCE_PATH_SAMPLES = 200
EMBED_REPLICATIONS = True  # whether to embed all replications into the input file or output separate files

input_file = sys.argv[1]
clothing = sys.argv[2]
bodypart = sys.argv[3]
output_file_embed = sys.argv[4]

# TODO: make these configurable
output_file_pattern = '%s_%s_%s.svg'  # bodypart, target_id, clothing

if 'Torso' == bodypart:
    xpath_shape = './svg:g[@id="Torso_"]/svg:g[@id="Torso_%s"]/svg:path[@class="skin torso"]/@d'  # TODO: formulate more general, independent of style
    xpath_outfit_container = '//svg:g[@id="Torso_Outfit_%s_"]' % (clothing)
    xpath_outfit = '//svg:g[@id="Torso_Outfit_%s_%s"]' % (clothing, '%s')
    target_ids = "Unnatural,Hourglass,Normal".split(",")
    reference_id = "Hourglass"
else:
    raise RuntimeError("Please specify a bodypart for clothing to replicate.")

tree = etree.parse(input_file)
ns = {'svg': 'http://www.w3.org/2000/svg'}

canvas = copy.deepcopy(tree)
for e in canvas.xpath('./svg:g', namespaces=ns) + canvas.xpath('./svg:path', namespaces=ns):
    # TODO: this should be "remove all objects, preserve document properties"
    e.getparent().remove(e)

def get_points(xpath_shape):
    """
    This function extracts reference paths by the given xpath selector.
    Each path is used to sample a fixed number of points.
    """
    paths_data = tree.xpath(xpath_shape, namespaces=ns)
    points = []
    path_length = None
    for path_data in paths_data:
        p = parse_path(path_data)
        points += [
            p.point(1.0 / float(REFERENCE_PATH_SAMPLES) * i)
            for i in range(REFERENCE_PATH_SAMPLES)
        ]
    if not points:
        raise RuntimeError(
            'No paths for reference points found by selector "%s".' % xpath_shape
        )
    return points

def point_movement(point, reference_points, target_points):
    """
    For a given point, finds the nearest point in the reference path.
    Gives distance vector from the nearest reference point to the
    respective target reference point.
    """
    distances = [abs(point - reference_point) for reference_point in reference_points]
    min_ref_dist_idx = min(enumerate(distances), key=lambda x: x[1])[0]
    movement = target_points[min_ref_dist_idx] - reference_points[min_ref_dist_idx]
    return movement

reference_points = get_points(xpath_shape % reference_id)
container = tree.xpath(xpath_outfit_container, namespaces=ns)
if len(container) != 1:
    raise RuntimeError('Outfit container selector "%s" does not yield exactly one layer.' % xpath_outfit_container)
container = container[0]
outfit_source = container.xpath(xpath_outfit % reference_id, namespaces=ns)
if len(outfit_source) != 1:
    raise RuntimeError(
        'Outfit source selector "%s" does not yield exactly one outfit layer in container selected by "%s".' % (
            xpath_outfit % reference_id, xpath_outfit_container))
outfit_source = outfit_source[0]

for target_id in target_ids:
    print(
        'Generating variant "%s" of clothing "%s" for bodypart "%s"...' %
        (target_id, clothing, bodypart)
    )
    outfit = copy.deepcopy(outfit_source)
    paths = outfit.xpath('./svg:path', namespaces=ns)
    if target_id == reference_id:
        print("This is the source variant. Skipping...")
    else:
        layerid = outfit.get('id').replace('_%s' % reference_id, '_%s' % target_id)
        outfit.set('id', layerid)
        outfit.set(etree.QName('http://www.inkscape.org/namespaces/inkscape', 'label'),
                   layerid)  # for the Inkscape-users
        target_points = get_points(xpath_shape % target_id)
        if len(reference_points) != len(target_points):
            raise RuntimeError(
                ('Different amounts of sampled points in reference "%s" and target "%s" paths. ' +
                 'Selector "%s" probably matches different number of paths in the two layers.') %
                (reference_id, target_id, xpath_shape)
            )
        for path in paths:
            path_data = path.get("d")
            p = parse_path(path_data)
            for segment in p:
                original_distance = abs(segment.end - segment.start)
                start_movement = point_movement(segment.start, reference_points, target_points)
                segment.start += start_movement
                end_movement = point_movement(segment.end, reference_points, target_points)
                segment.end += end_movement
                distance = abs(segment.end - segment.start)
                try:
                    # enhance position of CubicBezier control points
                    # amplification is relative to the distance gained by movement
                    segment.control1 += start_movement
                    segment.control1 += (segment.control1 - segment.start) * (distance / original_distance - 1.0)
                    segment.control2 += end_movement
                    segment.control2 += (segment.control2 - segment.end) * (distance / original_distance - 1.0)
                except AttributeError as ae:
                    # segment is not a CubicBezier
                    pass
            path.set("d", p.d())
        if EMBED_REPLICATIONS:
            container.append(outfit)
    if not EMBED_REPLICATIONS:
        container = copy.deepcopy(canvas).xpath('.', namespaces=ns)[0]
        container.append(outfit)

    if not EMBED_REPLICATIONS:
        svg = etree.tostring(container, pretty_print=True)
        with open((output_file_pattern % (bodypart, target_id, clothing)).lower(), 'wb') as f:
            f.write('<?xml version="1.0" encoding="UTF-8" standalone="no"?>\n'.encode("utf-8"))
            f.write(svg)

if EMBED_REPLICATIONS:
    svg = etree.tostring(tree, pretty_print=True)
    with open(output_file_embed, 'wb') as f:
        f.write('<?xml version="1.0" encoding="UTF-8" standalone="no"?>\n'.encode("utf-8"))
        f.write(svg)


code:
#!/usr/bin/env python3

"""
Application for splitting groups from one SVG file into separate files

Usage:
python3 vector_layer_split.py infile format outdir

Usage Example:
python3 vector_layer_split.py vector_source.svg tw ../src/art/vector/layers/
"""

import argparse
import copy
import os
import re

import lxml.etree as etree
import normalize_svg
import svg_split_utils

parser = argparse.ArgumentParser(
    description='Application for splitting groups from one SVG file into separate files.')
parser.add_argument('-o', '--output', dest='output_dir', required=True,
                    help='output directory')
parser.add_argument('-f', '--format', dest='output_format',
                    choices=['svg', 'tw'], default='svg', help='output format.')
parser.add_argument('-p', '--prefix', dest='prefix', default='',
                    help='Prepend this string to result file names')
parser.add_argument('input_file', metavar='FILENAME', nargs='+',
                    help='Input SVG file with layers')

args = parser.parse_args()
output_format = args.output_format
output_directory = args.output_dir

def split_file(input_file):
    tree = etree.parse(input_file)
    normalize_svg.fix(tree)

    ns = {
        'svg': 'http://www.w3.org/2000/svg',
        'inkscape': 'http://www.inkscape.org/namespaces/inkscape',
        'sodipodi': "http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd",
    }

    # strip undesired editor attributes from the tree
    etree.strip_attributes(tree, f"{{{ns['inkscape']}}}*", f"{{{ns['sodipodi']}}}*");

    # prepare output template
    template = copy.deepcopy(tree)
    root = template.getroot()

    # remove all svg root attributes except document size
    for a in root.attrib:
        if a != "viewBox":
            del root.attrib[a]

    # remove all content, including metadata
    # for twine output, style definitions are removed, too
    defs = None
    for e in root:
        if e.tag == etree.QName(ns['svg'], 'defs'):
            defs = e
        if (e.tag == etree.QName(ns['svg'], 'g') or
                e.tag == etree.QName(ns['svg'], 'metadata') or
                e.tag == etree.QName(ns['svg'], 'defs') or
                e.tag == etree.QName(ns['sodipodi'], 'namedview') or
                (output_format == 'tw' and e.tag == etree.QName(ns['svg'], 'style'))
        ):
            root.remove(e)
    # template preparation finished

    # prepare regex for later use
    regex_xmlns = re.compile(' xmlns[^ ]+')
    regex_space = re.compile(r'[>]\s+[<]')

    # find all groups
    layers = tree.xpath('//svg:g', namespaces=ns)
    for layer in layers:
        i = layer.get('id')
        if (  # disregard non-content groups
                i.endswith("_") or  # manually suppressed with underscore
                i.startswith("XMLID") or  # Illustrator generated group
                i.startswith("g")  # Inkscape generated group
        ):
            continue

        # create new canvas
        output = copy.deepcopy(template)
        # copy all shapes into template
        canvas = output.getroot()
        for e in layer:
            canvas.append(e)
        # represent template as SVG (binary string)
        svg = etree.tostring(output, pretty_print=False)
        # poor man's conditional defs insertion
        # TODO: extract only referenced defs (filters, gradients, ...)
        # TODO: detect necessity by traversing the elements. do not stupidly search in the string representation
        if "filter:" in svg.decode('utf-8'):
            # it seems there is a filter referenced in the generated SVG, re-insert defs from main document
            canvas.insert(0, defs)
            # re-generate output
            svg = etree.tostring(output, pretty_print=False)

        if output_format == 'tw':
            # remove unnecessary attributes
            # TODO: never generate unnecessary attributes in the first place
            svg = svg.decode('utf-8')
            svg = regex_xmlns.sub('', svg)
            svg = svg.replace('\n', '').replace('\r', '')  # print cannot be multi-line
            svg = regex_space.sub('><', svg)  # remove indentation
            svg = svg.replace('svg:', '')  # svg namespace was removed
            if "Boob" in i:  # internal groups are used for scaling
                svg = svg.replace('<g ', '<g data-transform="boob" ')  # boob art uses the boob scaling
            elif "Belly" in i:
                svg = svg.replace('<g ', '<g data-transform="belly" ')  # belly art uses the belly scaling
            elif "Balls" in i:
                svg = svg.replace('<g ', '<g data-transform="balls" ')  # balls art uses the balls scaling
            else:
                svg = svg.replace('<g ', '<g data-transform="art" ')  # otherwise use default scaling
        if not svg.endswith(os.linesep):
            svg += os.linesep
        svg = svg.encode('utf-8')

        # save SVG string to file
        svg_split_utils.save(layer, args.prefix, output_directory, output_format, svg)

if not os.path.exists(output_directory):
    os.makedirs(output_directory)

for f in args.input_file:
    split_file(f)


code:
#!/usr/bin/env python3

"""
Application for splitting groups from one SVG file into separate files

Usage:
python3 vector_layer_split.py infile format outdir

Usage Example:
python3 vector_layer_split.py vector_source.svg tw ../src/art/vector/layers/
"""

import argparse
import copy
import os
import re

import lxml.etree as etree
import normalize_svg
from lxml.etree import XMLParser, parse
import svg_split_utils

parser = argparse.ArgumentParser(
    description='Application for splitting groups from one SVG file into separate files.')
parser.add_argument('-o', '--output', dest='output_dir', required=True,
                    help='output directory')
parser.add_argument('-f', '--format', dest='output_format',
                    choices=['svg', 'tw'], default='svg', help='output format.')
parser.add_argument('-p', '--prefix', dest='prefix', default='',
                    help='Prepend this string to result file names')
parser.add_argument('input_file', metavar='FILENAME', nargs=1,
                    help='Input SVG file with layers')

args = parser.parse_args()

output_format = args.output_format
output_directory = args.output_dir
input_file = args.input_file[0]

if not os.path.exists(output_directory):
    os.makedirs(output_directory)

ns = {
    'svg': 'http://www.w3.org/2000/svg',
    'inkscape': 'http://www.inkscape.org/namespaces/inkscape',
    'sodipodi': "http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd",
}

p = XMLParser(huge_tree=True)
tree = parse(input_file, parser=p)
normalize_svg.fix(tree)

# strip undesired editor attributes from the tree
etree.strip_attributes(tree, f"{{{ns['inkscape']}}}*", f"{{{ns['sodipodi']}}}*")

# prepare output template
template = copy.deepcopy(tree)
root = template.getroot()

# remove all svg root attributes except document size
for a in root.attrib:
    if a != "viewBox":
        del root.attrib[a]

# remove all content, including metadata
# for twine output, style definitions are removed, too
defs = None
for e in root:
    if e.tag == etree.QName(ns['svg'], 'defs'):
        defs = e
    if (e.tag == etree.QName(ns['svg'], 'g') or
            e.tag == etree.QName(ns['svg'], 'metadata') or
            e.tag == etree.QName(ns['svg'], 'defs') or
            e.tag == etree.QName(ns['sodipodi'], 'namedview') or
            (output_format == 'tw' and e.tag == etree.QName(ns['svg'], 'style'))
    ):
        root.remove(e)
# template preparation finished

# prepare regex for later use
regex_xmlns = re.compile(' xmlns[^ ]+')
regex_space = re.compile(r'[>]\s+[<]')

# create dict with all needed definitions
gra_dict = {}
for gra in defs:
    if (gra.tag == etree.QName(ns['svg'], 'linearGradient') or
            gra.tag == etree.QName(ns['svg'], 'radialGradient') or
            gra.tag == etree.QName(ns['svg'], 'clipPath') or
            gra.tag == etree.QName(ns['svg'], 'mask') or
            gra.tag == etree.QName(ns['svg'], 'filter')):
        gra_id = gra.get('id')
        gra = etree.tostring(gra, pretty_print=False)
        gra = gra.decode('utf-8')
        gra_dict[gra_id] = gra
    # find all groups
layers = tree.xpath('//svg:g', namespaces=ns)
for layer in layers:
    i = layer.get('id')
    if (  # disregard non-content groups
            i.endswith("_") or  # manually suppressed with underscore
            i.startswith("XMLID") or  # Illustrator generated group
            i.startswith("g")  # Inkscape generated group
    ):
        continue
    # create new canvas
    output = copy.deepcopy(template)
    # copy all shapes into template
    canvas = output.getroot()
    for e in layer:
        canvas.append(e)
    # represent template as SVG (binary string)
    svg = etree.tostring(output, pretty_print=False)

    # replace <use> with the referenced object
    # this is a very poor way to do it. A more elegant way would be to insert
    # referenced object in the <def> part of the file, but then the colors of
    # objects with a class definition will not be changed.
    # As a substitute the <use> is overwritten with the object.
    # THIS WILL REMOVE ANY MODIFICATIONS (movement, clip-paths, etc.)
    # To prevent this the cloned object should be put into a group, and changes
    # made to the group.
    # TODO: If colors can be changed based on class in defs - put reference in defs instead
    if "<svg:use" in svg.decode('utf-8'):
        # get a reference map
        output_map = {c: p for p in canvas.iter() for c in p}
        # find use elements
        for elem in list(canvas.xpath('//svg:use', namespaces=ns)):
            # get referenced id
            def_id = elem.get('{http://www.w3.org/1999/xlink}href')
            def_id = def_id[1:]
            # loop through all layers to find the referenced element
            def_orig = None
            for def_l in layers:
                for e in (e for e in def_l if e.get('id') == def_id):
                    def_orig = copy.deepcopy(e)
                if def_orig is not None:
                    break
            parent_index = list(output_map).index(elem)
            output_map[elem].remove(elem)
            output_map[elem].insert(parent_index, def_orig)
        # Update svg
        svg = etree.tostring(output, pretty_print=False)

    # Conditional defs insertion - only inserts the needed defs
    svg_dec = svg.decode('utf-8')
    if any(x in svg_dec for x in ["filter", "Gradient", "mask=", "clip-path="]):
        moreDefsNeeded = True
        gra_str = '<defs>'
        while moreDefsNeeded:
            moreDefsNeeded = False
            s = svg.decode('utf-8')
            for key in gra_dict.keys():
                if ('#' + key in s or '#' + key in gra_str) and 'id="' + key not in gra_str:
                    moreDefsNeeded = True
                    gra_str += gra_dict[key]
        gra_str += '</defs>'
        canvas.insert(0, etree.fromstring(gra_str))
        # re-generate output
        svg = etree.tostring(output, pretty_print=False)

    if output_format == 'tw':
        # remove unnecessary attributes
        # TODO: never generate unnecessary attributes in the first place
        svg = svg.decode('utf-8')
        svg = regex_xmlns.sub('', svg)
        svg = svg.replace('\n', '').replace('\r', '')  # print cannot be multi-line
        svg = regex_space.sub('><', svg)  # remove indentation
        svg = svg.replace('svg:', '')  # svg namespace was removed

        if not svg.endswith(os.linesep):
            svg += os.linesep
        svg = svg.encode('utf-8')

    # save SVG string to file
    svg_split_utils.save(layer, args.prefix, output_directory, output_format, svg)