diff --git a/.classpath b/.classpath index 6dc0ab3ec..fb1a79e75 100644 --- a/.classpath +++ b/.classpath @@ -2,20 +2,20 @@ - + - + - + - + - + diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index fd85e7579..07af07995 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -24,7 +24,7 @@ jobs: - uses: actions/checkout@v4 - name: Set up JDK 1.8 - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: java-version: '8' distribution: 'temurin' @@ -36,19 +36,19 @@ jobs: echo "NI_VERSION=$(java -jar "NearInfinity.jar" -version 2>/dev/null | grep -Eo '[0-9]{8}')" >> "$GITHUB_OUTPUT" - name: Upload JAR artifact - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: NearInfinity-${{ steps.ni-build.outputs.NI_VERSION }} path: NearInfinity.jar - # Build and upload installer versions for Windows and macOS + # Build and upload installer versions for Windows, macOS-x86_64 and macOS-arm64 deploy-installer: if: ${{ github.repository == 'Argent77/NearInfinity' }} needs: deploy-jar strategy: fail-fast: false matrix: - os: [ windows-latest, macos-latest ] + os: [ windows-latest, macos-13, macos-14 ] java: [ '21' ] runs-on: ${{ matrix.os }} name: Create installer for ${{ matrix.os }}, JDK ${{ matrix.java }} @@ -58,7 +58,7 @@ jobs: uses: actions/checkout@v4 - name: Set up JDK - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: java-version: ${{ matrix.java }} distribution: 'temurin' @@ -77,7 +77,7 @@ jobs: # Preparations - name: Download JAR artifact - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: NearInfinity-${{ needs.deploy-jar.outputs.ni_version }} path: jar @@ -99,7 +99,7 @@ jobs: .\build-installer.cmd - name: Build installer (macos) - if: (matrix.os == 'macos-latest') + if: startsWith(matrix.os, 'macos-') run: | mv assets/redistributable/macos/package . mv assets/redistributable/macos/build.command . @@ -112,27 +112,34 @@ jobs: run: dir - name: List built files (macos) - if: (matrix.os == 'macos-latest') + if: startsWith(matrix.os, 'macos-') run: ls -l # Uploading - name: Upload portable artifact (windows) if: (matrix.os == 'windows-latest') - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: portable-windows path: NearInfinity-*.zip - name: Upload exe artifact (windows) if: (matrix.os == 'windows-latest') - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: installer-windows path: NearInfinity-*.exe - name: Upload pkg artifact (macos) - if: (matrix.os == 'macos-latest') - uses: actions/upload-artifact@v3 + if: (matrix.os == 'macos-13') + uses: actions/upload-artifact@v4 with: - name: installer-macos + name: installer-macos-x86_64 + path: NearInfinity-*.pkg + + - name: Upload pkg artifact (macos-arm64) + if: (matrix.os == 'macos-14') + uses: actions/upload-artifact@v4 + with: + name: installer-macos-arm64 path: NearInfinity-*.pkg diff --git a/build.xml b/build.xml index 0414892df..932571124 100644 --- a/build.xml +++ b/build.xml @@ -12,8 +12,8 @@ - - + + diff --git a/lib/flatlaf/flatlaf-3.1.1-javadoc.jar b/lib/flatlaf/flatlaf-3.1.1-javadoc.jar deleted file mode 100644 index 8554df047..000000000 Binary files a/lib/flatlaf/flatlaf-3.1.1-javadoc.jar and /dev/null differ diff --git a/lib/flatlaf/flatlaf-3.1.1-sources.jar b/lib/flatlaf/flatlaf-3.1.1-sources.jar deleted file mode 100644 index d56e521ad..000000000 Binary files a/lib/flatlaf/flatlaf-3.1.1-sources.jar and /dev/null differ diff --git a/lib/flatlaf/flatlaf-3.1.1.jar b/lib/flatlaf/flatlaf-3.1.1.jar deleted file mode 100644 index 2b13cdd93..000000000 Binary files a/lib/flatlaf/flatlaf-3.1.1.jar and /dev/null differ diff --git a/lib/flatlaf/flatlaf-3.4-javadoc.jar b/lib/flatlaf/flatlaf-3.4-javadoc.jar new file mode 100644 index 000000000..adc4e43b6 Binary files /dev/null and b/lib/flatlaf/flatlaf-3.4-javadoc.jar differ diff --git a/lib/flatlaf/flatlaf-3.4-sources.jar b/lib/flatlaf/flatlaf-3.4-sources.jar new file mode 100644 index 000000000..8ece5caf1 Binary files /dev/null and b/lib/flatlaf/flatlaf-3.4-sources.jar differ diff --git a/lib/flatlaf/flatlaf-3.4.jar b/lib/flatlaf/flatlaf-3.4.jar new file mode 100644 index 000000000..b469042c5 Binary files /dev/null and b/lib/flatlaf/flatlaf-3.4.jar differ diff --git a/lib/flatlaf/flatlaf-intellij-themes-3.1.1-javadoc.jar b/lib/flatlaf/flatlaf-intellij-themes-3.1.1-javadoc.jar deleted file mode 100644 index fef523a6b..000000000 Binary files a/lib/flatlaf/flatlaf-intellij-themes-3.1.1-javadoc.jar and /dev/null differ diff --git a/lib/flatlaf/flatlaf-intellij-themes-3.1.1-sources.jar b/lib/flatlaf/flatlaf-intellij-themes-3.1.1-sources.jar deleted file mode 100644 index 9c22ea48e..000000000 Binary files a/lib/flatlaf/flatlaf-intellij-themes-3.1.1-sources.jar and /dev/null differ diff --git a/lib/flatlaf/flatlaf-intellij-themes-3.1.1.jar b/lib/flatlaf/flatlaf-intellij-themes-3.1.1.jar deleted file mode 100644 index 8fd531bd9..000000000 Binary files a/lib/flatlaf/flatlaf-intellij-themes-3.1.1.jar and /dev/null differ diff --git a/lib/flatlaf/flatlaf-intellij-themes-3.4-javadoc.jar b/lib/flatlaf/flatlaf-intellij-themes-3.4-javadoc.jar new file mode 100644 index 000000000..6981e466f Binary files /dev/null and b/lib/flatlaf/flatlaf-intellij-themes-3.4-javadoc.jar differ diff --git a/lib/flatlaf/flatlaf-intellij-themes-3.4-sources.jar b/lib/flatlaf/flatlaf-intellij-themes-3.4-sources.jar new file mode 100644 index 000000000..88e9a863e Binary files /dev/null and b/lib/flatlaf/flatlaf-intellij-themes-3.4-sources.jar differ diff --git a/lib/flatlaf/flatlaf-intellij-themes-3.4.jar b/lib/flatlaf/flatlaf-intellij-themes-3.4.jar new file mode 100644 index 000000000..bfa8bb889 Binary files /dev/null and b/lib/flatlaf/flatlaf-intellij-themes-3.4.jar differ diff --git a/lib/rsyntaxtextarea/rsyntaxtextarea-3.3.2-sources.jar b/lib/rsyntaxtextarea/rsyntaxtextarea-3.4.0-sources.jar similarity index 58% rename from lib/rsyntaxtextarea/rsyntaxtextarea-3.3.2-sources.jar rename to lib/rsyntaxtextarea/rsyntaxtextarea-3.4.0-sources.jar index cba94e097..b636281db 100644 Binary files a/lib/rsyntaxtextarea/rsyntaxtextarea-3.3.2-sources.jar and b/lib/rsyntaxtextarea/rsyntaxtextarea-3.4.0-sources.jar differ diff --git a/lib/rsyntaxtextarea/rsyntaxtextarea.jar b/lib/rsyntaxtextarea/rsyntaxtextarea.jar index 9c64ebfee..d09030644 100644 Binary files a/lib/rsyntaxtextarea/rsyntaxtextarea.jar and b/lib/rsyntaxtextarea/rsyntaxtextarea.jar differ diff --git a/src/org/infinity/AppOption.java b/src/org/infinity/AppOption.java index 16034647b..76f6071d4 100644 --- a/src/org/infinity/AppOption.java +++ b/src/org/infinity/AppOption.java @@ -190,7 +190,7 @@ public class AppOption { public static final AppOption TEXT_TAB_SIZE = new AppOption(OptionsMenuItem.OPTION_TEXT_TABSIZE, "Tab Size", 1); // Category: BCS and BAF - /** Menu Options > BCS and BAF: BcsColorScheme (Integer, Default: 6) */ + /** Menu Options > BCS and BAF: BcsColorScheme (Integer, Default: last item in list) */ public static final AppOption BCS_COLOR_SCHEME = new AppOption(OptionsMenuItem.OPTION_BCS_COLORSCHEME, "BCS Color Scheme", OptionsMenuItem.getBcsColorSchemes().size() - 1); /** Menu Options > BCS and BAF: BcsSyntaxHighlighting (Boolean, Default: true) */ @@ -230,6 +230,12 @@ public class AppOption { /** Menu Options > Misc. Types: LuaSyntaxHighlighting (Boolean, Default: true) */ public static final AppOption LUA_SYNTAX_HIGHLIGHTING = new AppOption(OptionsMenuItem.OPTION_LUA_SYNTAXHIGHLIGHTING, "Enable LUA Syntax Highlighting", true); + /** Menu Options > Misc. Types: MenuColorScheme (Integer, Default: 0) */ + public static final AppOption MENU_COLOR_SCHEME = new AppOption(OptionsMenuItem.OPTION_MENU_COLORSCHEME, + "MENU Color Scheme", 0); + /** Menu Options > Misc. Types: MenuSyntaxHighlighting (Boolean, Default: true) */ + public static final AppOption MENU_SYNTAX_HIGHLIGHTING = new AppOption(OptionsMenuItem.OPTION_MENU_SYNTAXHIGHLIGHTING, + "Enable MENU Syntax Highlighting", true); /** Menu Options > Misc. Types: SqlColorScheme (Integer, Default: 0) */ public static final AppOption SQL_COLOR_SCHEME = new AppOption(OptionsMenuItem.OPTION_SQL_COLORSCHEME, "SQL Color Scheme", 0); @@ -250,6 +256,12 @@ public class AppOption { "Enable WeiDU Syntax Highlighting", true); // Category: Dialog Tree Viewer + /** Menu Options > Dialog Tree Viewer: DlgColorScheme (Integer, Default: last item in list) */ + public static final AppOption DLG_COLOR_SCHEME = new AppOption(OptionsMenuItem.OPTION_DLG_COLORSCHEME, + "Color Scheme", OptionsMenuItem.getDlgColorSchemes().size() - 1); + /** Menu Options > Dialog Tree Viewer: DlgSyntaxHighlighting (Boolean, Default: true) */ + public static final AppOption DLG_SYNTAX_HIGHLIGHTING = new AppOption(OptionsMenuItem.OPTION_DLG_SYNTAXHIGHLIGHTING, + "Enable Syntax Highlighting", true); /** Menu Options > Dialog Tree Viewer: DlgShowIcons (Boolean, Default: true) */ public static final AppOption DLG_SHOW_ICONS = new AppOption(OptionsMenuItem.OPTION_SHOWICONS, "Show Icons", true); /** Menu Options > Dialog Tree Viewer: DlgSortStatesByWeight (Boolean, Default: true) */ diff --git a/src/org/infinity/NearInfinity.java b/src/org/infinity/NearInfinity.java index f6517e91f..b1efeaca9 100644 --- a/src/org/infinity/NearInfinity.java +++ b/src/org/infinity/NearInfinity.java @@ -136,7 +136,7 @@ public final class NearInfinity extends JFrame implements ActionListener, ViewableContainer { // the current Near Infinity version - private static final String VERSION = "v2.4-20231231"; + private static final String VERSION = "v2.4-20240424"; // the minimum supported Java version private static final int JAVA_VERSION_MIN = 8; diff --git a/src/org/infinity/check/StructChecker.java b/src/org/infinity/check/StructChecker.java index 15abd7a2b..320639028 100644 --- a/src/org/infinity/check/StructChecker.java +++ b/src/org/infinity/check/StructChecker.java @@ -564,6 +564,7 @@ public int getOffset() { return offset; } + @SuppressWarnings("unused") public String getMessage() { return errorMsg; } diff --git a/src/org/infinity/datatype/ResourceRef.java b/src/org/infinity/datatype/ResourceRef.java index c84033364..a7d10e2d1 100644 --- a/src/org/infinity/datatype/ResourceRef.java +++ b/src/org/infinity/datatype/ResourceRef.java @@ -203,6 +203,7 @@ public void mouseClicked(MouseEvent event) { bView.addActionListener(this); bPlay = new JButton("Play", Icons.ICON_PLAY_16.getIcon()); bPlay.addActionListener(this); + bPlay.setVisible(ResourceEntry.isSound(types)); list.addListSelectionListener(this); setResourceEntryUpdated(list.getSelectedValue()); diff --git a/src/org/infinity/gui/InfinityTextArea.java b/src/org/infinity/gui/InfinityTextArea.java index 5365c1a2f..30df0a300 100644 --- a/src/org/infinity/gui/InfinityTextArea.java +++ b/src/org/infinity/gui/InfinityTextArea.java @@ -40,6 +40,7 @@ import org.infinity.resource.text.modes.BCSTokenMaker; import org.infinity.resource.text.modes.GLSLTokenMaker; import org.infinity.resource.text.modes.INITokenMaker; +import org.infinity.resource.text.modes.MenuTokenMaker; import org.infinity.resource.text.modes.TLKTokenMaker; import org.infinity.resource.text.modes.WeiDULogTokenMaker; import org.infinity.util.Misc; @@ -54,6 +55,8 @@ public enum Language { NONE(SyntaxConstants.SYNTAX_STYLE_NONE), /** Select BCS highlighting. */ BCS(BCSTokenMaker.SYNTAX_STYLE_BCS), + /** Select DLG tree viewer trigger/action highlighting. */ + DLG(BCSTokenMaker.SYNTAX_STYLE_BCS), /** Select TLK highlighting. */ TLK(TLKTokenMaker.SYNTAX_STYLE_TLK), /** Select GLSL highlighting. */ @@ -62,6 +65,8 @@ public enum Language { INI(INITokenMaker.SYNTAX_STYLE_INI), /** Select LUA highlighting. */ LUA(SyntaxConstants.SYNTAX_STYLE_LUA), + /** Select MENU highlighting. */ + MENU(MenuTokenMaker.SYNTAX_STYLE_MENU), /** Select SQL highlighting. */ SQL(SyntaxConstants.SYNTAX_STYLE_SQL), /** Select WeiDU.log highlighting. */ @@ -163,6 +168,8 @@ public String getScheme() { GLSLTokenMaker.class.getCanonicalName()); ((AbstractTokenMakerFactory) TokenMakerFactory.getDefaultInstance()).putMapping(INITokenMaker.SYNTAX_STYLE_INI, INITokenMaker.class.getCanonicalName()); + ((AbstractTokenMakerFactory) TokenMakerFactory.getDefaultInstance()).putMapping(MenuTokenMaker.SYNTAX_STYLE_MENU, + MenuTokenMaker.class.getCanonicalName()); } private final SortedMap gutterIcons = new TreeMap<>(); @@ -342,6 +349,11 @@ public static void applyExtendedSettings(RSyntaxTextArea edit, Language language schemePath = BrowserMenuBar.getInstance().getOptions().getBcsColorScheme(); } break; + case DLG: + if (BrowserMenuBar.isInstantiated()) { + schemePath = BrowserMenuBar.getInstance().getOptions().getDlgColorScheme(); + } + break; case TLK: if (BrowserMenuBar.isInstantiated()) { schemePath = BrowserMenuBar.getInstance().getOptions().getTlkColorScheme(); @@ -362,6 +374,11 @@ public static void applyExtendedSettings(RSyntaxTextArea edit, Language language schemePath = BrowserMenuBar.getInstance().getOptions().getLuaColorScheme(); } break; + case MENU: + if (BrowserMenuBar.isInstantiated()) { + schemePath = BrowserMenuBar.getInstance().getOptions().getMenuColorScheme(); + } + break; case SQL: if (BrowserMenuBar.isInstantiated()) { schemePath = BrowserMenuBar.getInstance().getOptions().getSqlColorScheme(); diff --git a/src/org/infinity/gui/OpenFileFrame.java b/src/org/infinity/gui/OpenFileFrame.java index 031dcaaa8..d794d8dbf 100644 --- a/src/org/infinity/gui/OpenFileFrame.java +++ b/src/org/infinity/gui/OpenFileFrame.java @@ -124,40 +124,41 @@ public void mouseClicked(MouseEvent event) { gbc.weightx = 1.0; gbc.gridwidth = GridBagConstraints.REMAINDER; gbc.fill = GridBagConstraints.HORIZONTAL; - gbc.insets = new Insets(3, 3, 3, 6); + gbc.insets = new Insets(6, 4, 3, 8); gbl.setConstraints(rbExternal, gbc); pane.add(rbExternal); gbc.fill = GridBagConstraints.HORIZONTAL; gbc.weightx = 1.0; gbc.gridwidth = 1; - gbc.insets = new Insets(0, 12, 3, 0); + gbc.insets = new Insets(0, 8, 3, 0); gbl.setConstraints(tfExternalName, gbc); pane.add(tfExternalName); gbc.fill = GridBagConstraints.NONE; gbc.weightx = 0.0; gbc.gridwidth = GridBagConstraints.REMAINDER; - gbc.insets = new Insets(0, 3, 3, 6); + gbc.insets = new Insets(0, 3, 3, 8); gbl.setConstraints(bExternalBrowse, gbc); pane.add(bExternalBrowse); gbc.fill = GridBagConstraints.BOTH; gbc.weightx = 1.0; gbc.weighty = 1.0; - gbc.insets = new Insets(0, 12, 3, 6); + gbc.insets = new Insets(0, 8, 3, 8); gbl.setConstraints(lExternalDrop, gbc); pane.add(lExternalDrop); gbc.weighty = 0.0; + gbc.weightx = 1.0; gbc.fill = GridBagConstraints.HORIZONTAL; - gbc.insets = new Insets(9, 3, 3, 6); + gbc.insets = new Insets(9, 4, 3, 8); gbl.setConstraints(rbInternal, gbc); pane.add(rbInternal); gbc.weighty = 3.0; gbc.fill = GridBagConstraints.BOTH; - gbc.insets = new Insets(0, 12, 3, 6); + gbc.insets = new Insets(0, 8, 3, 8); gbl.setConstraints(lpInternal, gbc); pane.add(lpInternal); @@ -167,18 +168,20 @@ public void mouseClicked(MouseEvent event) { gbc.fill = GridBagConstraints.HORIZONTAL; gbc.weighty = 0.0; - gbc.insets = new Insets(3, 6, 0, 6); + gbc.insets = new Insets(3, 8, 0, 8); gbl.setConstraints(bPanel, gbc); pane.add(bPanel); gbc.insets.top = 0; + gbc.insets.bottom = 6; gbc.anchor = GridBagConstraints.CENTER; gbc.fill = GridBagConstraints.NONE; gbl.setConstraints(cbStayOpen, gbc); pane.add(cbStayOpen); - setSize(330, 400); + pack(); + setMinimumSize(getSize()); Center.center(this, NearInfinity.getInstance().getBounds()); } diff --git a/src/org/infinity/gui/PreferencesDialog.java b/src/org/infinity/gui/PreferencesDialog.java index 4d7465c49..a11c3ddc9 100644 --- a/src/org/infinity/gui/PreferencesDialog.java +++ b/src/org/infinity/gui/PreferencesDialog.java @@ -345,6 +345,22 @@ public String toString() { "Enables syntax highlighting for LUA resources.", AppOption.LUA_SYNTAX_HIGHLIGHTING) .setOnCreated(this::luaSyntaxHighlightingOnCreated).setOnAction(this::luaSyntaxHighlightingOnAction) ), + OptionGroup.create("MENU", + OptionGroupBox.create(AppOption.MENU_COLOR_SCHEME.getName(), AppOption.MENU_COLOR_SCHEME.getLabel(), + "Select a color scheme for MENU resources.

" + + "Default: A general-purpose default color scheme.
" + + "Dark: A dark scheme based off of Notepad++'s Obsidian theme.
" + + "Druid: A dark green color scheme.
" + + "Eclipse: Mimics the default color scheme of the Eclipse IDE.
" + + "IntelliJ IDEA: Mimics the default color scheme of IntelliJ IDEA.
" + + "Monokai: A dark color scheme inspired by \"Monokai\".
" + + "Visual Studio: Mimics the default color scheme of Microsoft Visual Studio.

", + 0, OptionsMenuItem.getColorSchemes().toArray(new OptionsMenuItem.ColorScheme[0]), + AppOption.MENU_COLOR_SCHEME), + OptionCheckBox.create(AppOption.MENU_SYNTAX_HIGHLIGHTING.getName(), AppOption.MENU_SYNTAX_HIGHLIGHTING.getLabel(), + "Enables syntax highlighting for MENU resources.", AppOption.MENU_SYNTAX_HIGHLIGHTING) + .setOnCreated(this::menuSyntaxHighlightingOnCreated).setOnAction(this::menuSyntaxHighlightingOnAction) + ), OptionGroup.create("SQL", OptionGroupBox.create(AppOption.SQL_COLOR_SCHEME.getName(), AppOption.SQL_COLOR_SCHEME.getLabel(), "Select a color scheme for SQL resources.

" @@ -418,6 +434,19 @@ public String toString() { ), OptionCategory.create(Category.DIALOG_TREE_VIEWER, OptionGroup.createDefault( + OptionGroupBox.create(AppOption.DLG_COLOR_SCHEME.getName(), AppOption.DLG_COLOR_SCHEME.getLabel(), + "Select a color scheme for display of dialog triggers and actions.

" + + "Default: A general-purpose default color scheme.
" + + "Eclipse: Mimics the default color scheme of the Eclipse IDE.
" + + "IntelliJ IDEA: Mimics the default color scheme of IntelliJ IDEA.
" + + "Visual Studio: Mimics the default color scheme of Microsoft Visual Studio.
" + + "BCS Light: A color scheme that is based on the WeiDU Syntax Highlighter " + + "for Notepad++.

", + 0, OptionsMenuItem.getDlgColorSchemes().toArray(new OptionsMenuItem.ColorScheme[0]), + AppOption.DLG_COLOR_SCHEME), + OptionCheckBox.create(AppOption.DLG_SYNTAX_HIGHLIGHTING.getName(), AppOption.DLG_SYNTAX_HIGHLIGHTING.getLabel(), + "Enables syntax highlighting for dialog triggers and actions.", AppOption.DLG_SYNTAX_HIGHLIGHTING) + .setOnCreated(this::dlgSyntaxHighlightingOnCreated).setOnAction(this::dlgSyntaxHighlightingOnAction), OptionCheckBox.create(AppOption.DLG_SHOW_ICONS.getName(), AppOption.DLG_SHOW_ICONS.getLabel(), "Enable this option to show small icons in front of dialog states and responses in the dialog tree view.", AppOption.DLG_SHOW_ICONS), @@ -1238,6 +1267,17 @@ private boolean luaSyntaxHighlightingOnAction(OptionCheckBox cb) { return true; } + /** onCreated() function for {@link AppOption#MENU_SYNTAX_HIGHLIGHTING}. */ + private void menuSyntaxHighlightingOnCreated(OptionCheckBox cb) { + setOptionUiEnabled(AppOption.MENU_COLOR_SCHEME.getName(), cb.getUiCheckBox().isSelected()); + } + + /** onAction() function for {@link AppOption#MENU_SYNTAX_HIGHLIGHTING}. */ + private boolean menuSyntaxHighlightingOnAction(OptionCheckBox cb) { + setOptionUiEnabled(AppOption.MENU_COLOR_SCHEME.getName(), cb.getUiCheckBox().isSelected()); + return true; + } + /** onCreated() function for {@link AppOption#SQL_SYNTAX_HIGHLIGHTING}. */ private void sqlSyntaxHighlightingOnCreated(OptionCheckBox cb) { setOptionUiEnabled(AppOption.SQL_COLOR_SCHEME.getName(), cb.getUiCheckBox().isSelected()); @@ -1271,6 +1311,17 @@ private boolean weiduSyntaxHighlightingOnAction(OptionCheckBox cb) { return true; } + /** onCreated() function for {@link AppOption#DLG_SYNTAX_HIGHLIGHTING}. */ + private void dlgSyntaxHighlightingOnCreated(OptionCheckBox cb) { + setOptionUiEnabled(AppOption.DLG_COLOR_SCHEME.getName(), cb.getUiCheckBox().isSelected()); + } + + /** onAction() function for {@link AppOption#DLG_SYNTAX_HIGHLIGHTING}. */ + private boolean dlgSyntaxHighlightingOnAction(OptionCheckBox cb) { + setOptionUiEnabled(AppOption.DLG_COLOR_SCHEME.getName(), cb.getUiCheckBox().isSelected()); + return true; + } + /** Helper method for setting the enabled state of a given option component. */ private void setOptionUiEnabled(String optionName, boolean enable) { OptionBase option = optionRoot.findOption(optionName); diff --git a/src/org/infinity/gui/ResourceTree.java b/src/org/infinity/gui/ResourceTree.java index 05f751c4e..5049c8d32 100644 --- a/src/org/infinity/gui/ResourceTree.java +++ b/src/org/infinity/gui/ResourceTree.java @@ -668,7 +668,8 @@ private void maybeShowPopup(MouseEvent e) { private final class TreePopupMenu extends JPopupMenu implements ActionListener, PopupMenuListener { private final JMenuItem miOpen = new JMenuItem("Open"); - private final JMenuItem miOpennew = new JMenuItem("Open in new window"); + private final JMenuItem miOpenNew = new JMenuItem("Open in new window"); + private final JMenuItem miOpenBiffedNew = new JMenuItem("Open biffed resource in new window"); private final JMenuItem miReference = new JMenuItem("Find references"); private final JMenuItem miExport = new JMenuItem("Export"); private final JMenuItem miAddCopy = new JMenuItem("Add copy of"); @@ -681,8 +682,8 @@ private final class TreePopupMenu extends JPopupMenu implements ActionListener, miReference.setEnabled(false); miZip.setToolTipText("Create a zip archive out of the selected saved game."); Font fnt = miOpen.getFont().deriveFont(Font.PLAIN); - for (JMenuItem mi : new JMenuItem[] { miOpen, miOpennew, miReference, miExport, miZip, miAddCopy, miRename, - miDelete, miRestore }) { + for (JMenuItem mi : new JMenuItem[] { miOpen, miOpenNew, miOpenBiffedNew, miReference, miExport, + miZip, miAddCopy, miRename, miDelete, miRestore }) { add(mi); mi.addActionListener(this); mi.setFont(fnt); @@ -701,7 +702,7 @@ private ResourceEntry getResourceEntry() { @Override public void actionPerformed(ActionEvent event) { showResource = true; - ResourceEntry node = getResourceEntry(); + final ResourceEntry node = getResourceEntry(); if (event.getSource() == miOpen && node != null) { if (prevNextNode != null) { prevStack.push(prevNextNode); @@ -712,11 +713,24 @@ public void actionPerformed(ActionEvent event) { prevNextNode = node; shownResource = node; NearInfinity.getInstance().setViewable(ResourceFactory.getResource(node)); - } else if (event.getSource() == miOpennew && node != null) { + } else if (event.getSource() == miOpenNew && node != null) { Resource res = ResourceFactory.getResource(node); if (res != null) { new ViewFrame(NearInfinity.getInstance(), res); } + } else if (event.getSource() == miOpenBiffedNew && node != null) { + try { + final BIFFResourceEntry biffedNode = + new BIFFResourceEntry(ResourceFactory.getKeyfile().getResourceEntry(node.getResourceName()), false); + final Resource res = ResourceFactory.getResource(biffedNode); + if (res != null) { + new ViewFrame(NearInfinity.getInstance(), res); + } + } catch (NullPointerException e) { + System.err.println("Does not exist in BIFF: " + node); + JOptionPane.showMessageDialog(NearInfinity.getInstance(), + "Does not exist in BIFF: " + node, "Error", JOptionPane.ERROR_MESSAGE); + } } else if (event.getSource() == miReference && node != null) { Resource res = ResourceFactory.getResource(node); if (res != null && res instanceof Referenceable) { @@ -764,7 +778,14 @@ public void actionPerformed(ActionEvent event) { @Override public void popupMenuWillBecomeVisible(PopupMenuEvent event) { - ResourceEntry entry = getResourceEntry(); + final ResourceEntry entry = getResourceEntry(); + + boolean biffEnable = + (!BrowserMenuBar.getInstance().getOptions().ignoreOverrides() || + BrowserMenuBar.getInstance().getOptions().getOverrideMode() == OverrideMode.InOverride) && + entry.hasOverride(); + miOpenBiffedNew.setEnabled(biffEnable); + Class cls = ResourceFactory.getResourceType(entry); miReference.setEnabled(cls != null && Referenceable.class.isAssignableFrom(cls)); miRename.setEnabled(entry instanceof FileResourceEntry); diff --git a/src/org/infinity/gui/ScriptTextArea.java b/src/org/infinity/gui/ScriptTextArea.java index 50f6397fe..9e2dfa144 100644 --- a/src/org/infinity/gui/ScriptTextArea.java +++ b/src/org/infinity/gui/ScriptTextArea.java @@ -76,12 +76,26 @@ private enum IconType { private Signatures triggers; private Signatures actions; + /** + * Constructs a new script text area with BCS language settings. + */ public ScriptTextArea() { + this(Language.BCS, + BrowserMenuBar.isInstantiated() && BrowserMenuBar.getInstance().getOptions().getBcsSyntaxHighlightingEnabled()); + } + + /** + * Constructs a new script text area with customized language settings. + * + * @param language {@link Language} for syntax highlighting. + * @param enabled Whether syntax highlighting should be applied. + */ + public ScriptTextArea(Language language, boolean enabled) { super(true); Language lang; - if (BrowserMenuBar.isInstantiated() && BrowserMenuBar.getInstance().getOptions().getBcsSyntaxHighlightingEnabled()) { - lang = Language.BCS; + if (enabled && language != null) { + lang = language; } else { lang = Language.NONE; } diff --git a/src/org/infinity/gui/SortableTable.java b/src/org/infinity/gui/SortableTable.java index 80ec40fba..b1be0ec35 100644 --- a/src/org/infinity/gui/SortableTable.java +++ b/src/org/infinity/gui/SortableTable.java @@ -300,13 +300,20 @@ public void removeTableModelListener(TableModelListener l) { @SuppressWarnings({ "unchecked", "rawtypes" }) @Override public int compare(TableItem o1, TableItem o2) { - int res; + Integer res = null; final Object item1 = o1.getObjectAt(sortByColumn); final Object item2 = o2.getObjectAt(sortByColumn); if (item1 instanceof Comparable && item2 instanceof Comparable) { - res = ((Comparable) item1).compareTo(item2); - } else { + // best case: both items can be compared directly + try { + res = ((Comparable) item1).compareTo(item2); + } catch (Exception e) { + // ignored + } + } + if (res == null) { + // fall-back solution: compare by string final String s1 = Objects.toString(item1, ""); final String s2 = Objects.toString(item2, ""); diff --git a/src/org/infinity/gui/StructViewer.java b/src/org/infinity/gui/StructViewer.java index 32ffb3406..518d9b2d2 100644 --- a/src/org/infinity/gui/StructViewer.java +++ b/src/org/infinity/gui/StructViewer.java @@ -21,6 +21,8 @@ import java.awt.event.ComponentListener; import java.awt.event.ItemEvent; import java.awt.event.ItemListener; +import java.awt.event.KeyAdapter; +import java.awt.event.KeyEvent; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.awt.print.PageFormat; @@ -30,6 +32,7 @@ import java.nio.ByteBuffer; import java.util.HashMap; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -50,6 +53,7 @@ import javax.swing.ListSelectionModel; import javax.swing.SwingConstants; import javax.swing.SwingUtilities; +import javax.swing.Timer; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; import javax.swing.event.ListSelectionEvent; @@ -155,19 +159,19 @@ public final class StructViewer extends JPanel implements ListSelectionListener, private final JMenuItem miCut = createMenuItem(CMD_CUT, "Cut", Icons.ICON_CUT_16.getIcon(), this); private final JMenuItem miCopy = createMenuItem(CMD_COPY, "Copy", Icons.ICON_COPY_16.getIcon(), this); private final JMenuItem miPaste = createMenuItem(CMD_PASTE, "Paste", Icons.ICON_PASTE_16.getIcon(), this); - private final JMenuItem miToHex = createMenuItem(CMD_TOHEX, "Edit as hex", Icons.ICON_REFRESH_16.getIcon(), this); + private final JMenuItem miToHex = createMenuItem(CMD_TOHEX, "Edit as hexadecimal data", Icons.ICON_REFRESH_16.getIcon(), this); private final JMenuItem miToString = createMenuItem(CMD_TOSTRING, "Edit as string", Icons.ICON_REFRESH_16.getIcon(), this); - private final JMenuItem miToBin = createMenuItem(CMD_TOBIN, "Edit as binary", Icons.ICON_REFRESH_16.getIcon(), this); - private final JMenuItem miToDec = createMenuItem(CMD_TODEC, "Edit as decimal", Icons.ICON_REFRESH_16.getIcon(), this); + private final JMenuItem miToBin = createMenuItem(CMD_TOBIN, "Edit as binary data", Icons.ICON_REFRESH_16.getIcon(), this); + private final JMenuItem miToDec = createMenuItem(CMD_TODEC, "Edit as decimal data", Icons.ICON_REFRESH_16.getIcon(), this); private final JMenuItem miToInt = createMenuItem(CMD_TOINT, "Edit as number", Icons.ICON_REFRESH_16.getIcon(), this); - private final JMenuItem miToHexInt = createMenuItem(CMD_TOHEXINT, "Edit as hex number", Icons.ICON_REFRESH_16.getIcon(), this); + private final JMenuItem miToHexInt = createMenuItem(CMD_TOHEXINT, "Edit as hexadecimal number", Icons.ICON_REFRESH_16.getIcon(), this); private final JMenuItem miToFlags = createMenuItem(CMD_TOFLAGS, "Edit as bit field", Icons.ICON_REFRESH_16.getIcon(), this); private final JMenuItem miReset = createMenuItem(CMD_RESET, "Reset field type", Icons.ICON_REFRESH_16.getIcon(), this); private final JMenuItem miAddToAdvSearch = createMenuItem(CMD_ADD_ADV_SEARCH, "Add to Advanced Search", Icons.ICON_FIND_16.getIcon(), this); private final JMenuItem miGotoOffset = createMenuItem(CMD_GOTO_OFFSET, "Go to offset", null, this); private final JMenuItem miShowInTree = createMenuItem(CMD_SHOW_IN_TREE, "Show in tree", Icons.ICON_SELECT_IN_TREE_16.getIcon(), this); private final JMenuItem miShowViewer = createMenuItem(CMD_SHOWVIEWER, "Show in viewer", Icons.ICON_ROW_INSERT_AFTER_16.getIcon(), this); - private final JMenuItem miShowNewViewer = createMenuItem(CMD_COPYVALUE, "Show in new viewer", null, this); + private final JMenuItem miShowNewViewer = createMenuItem(CMD_SHOWNEWVIEWER, "Show in new viewer", null, this); private final JMenu miToResref = createResrefMenu(CMD_TORESLIST, "Edit as resref", Profile.getAvailableResourceTypes(), Icons.ICON_REFRESH_16.getIcon(), this); private final JPanel lowerpanel = new JPanel(cards); private final JPanel editpanel = new JPanel(); @@ -298,6 +302,7 @@ public Component getTableCellRendererComponent(JTable table, Object value, boole popupmenu.add(miShowNewViewer); } table.addMouseListener(new PopupListener()); + table.addKeyListener(new TableKeyListener()); miCopyValue.setEnabled(false); miPasteValue.setEnabled(false); miCut.setEnabled(false); @@ -1538,4 +1543,167 @@ public int print(Graphics graphics, PageFormat pageFormat, int pageIndex) throws return Printable.PAGE_EXISTS; } } + + /** + * This class implements a quick search feature that jumps to the next table entry matching entered characters. + */ + private class TableKeyListener extends KeyAdapter { + private static final int TIMER_DELAY = 1000; + + private final Timer timer; + private String currentKey; + + public TableKeyListener() { + this.currentKey = ""; + this.timer = new Timer(TIMER_DELAY, e -> resetKey()); + this.timer.setRepeats(false); + } + + public void keyTyped(KeyEvent event) { + if (KeyEvent.VK_BACK_SPACE == event.getKeyCode()) { + removeKeyChar(); + } else if (KeyEvent.VK_ENTER != event.getKeyCode()) { + appendKeyChar(event.getKeyChar()); + } + timerReset(); + selectEntry(table.getSelectedColumn(), table.getSelectedRow(), currentKey); + } + + /** Adds the specified character to the search string. */ + private void appendKeyChar(char ch) { + synchronized (timer) { + // skip control characters + if (ch >= ' ') { + currentKey += Character.toString(ch); + } + } + } + + /** Removes the last charcater from the search string. */ + private void removeKeyChar() { + synchronized (timer) { + if (currentKey.length() > 0) { + currentKey = currentKey.substring(0, currentKey.length() - 1); + } + } + } + + /** Clears the search string. */ + private void resetKey() { + synchronized (timer) { + currentKey = ""; + } + } + + /** Restarts the input timer. */ + private void timerReset() { + if (timer.isRunning()) { + timer.restart(); + } else { + timer.start(); + } + } + + /** Returns {@code true} only if the specified column points to the "Attribute" column. */ + private boolean isAttributeColumn(int col) { + if (col >= 0) { + final String colName = table.getModel().getColumnName(col); + return AbstractStruct.COLUMN_ATTRIBUTE.equals(colName); + } + return false; + } + + /** Returns {@code true} only if the specified column points to the "Offset" column. */ + private boolean isOffsetColumn(int col) { + if (col >= 0) { + final String colName = table.getModel().getColumnName(col); + return AbstractStruct.COLUMN_OFFSET.equals(colName); + } + return false; + } + + /** Selects the next matching row based on the given arguments. */ + private void selectEntry(int col, int row, String key) { + row = Math.max(-1, row); + col = Math.max(0, col); + + // only enabled for "Attribute" and "Offset" columns + if (!isAttributeColumn(col) && !isOffsetColumn(col)) { + return; + } + + if (key == null) { + key = ""; + } + + final String curKey; + final String regex, replacement; + if (isAttributeColumn(col)) { + // "Attribute" column + curKey = key.replaceFirst("^ +", ""); + regex = null; + replacement = null; + } else { + // "Offset" column + if (key.startsWith("+")) { + final String ofsPattern = "[-0-9a-fA-F]+ h"; + regex = ofsPattern + " \\((" + ofsPattern + ")\\)"; + replacement = "$1"; + } else { + regex = null; + replacement = null; + } + curKey = key.replaceAll("[^0-9a-fA-F]+", ""); + } + + if (curKey.isEmpty()) { + return; + } + + for (int y = row, count = table.getRowCount(); y < count; y++) { + if (findMatch(y, col, curKey, regex, replacement)) { + table.getSelectionModel().setSelectionInterval(y, y); + return; + } + } + + // wrap around + for (int y = 0; y < row; y++) { + if (findMatch(y, col, curKey, regex, replacement)) { + table.getSelectionModel().setSelectionInterval(y, y); + return; + } + } + } + + /** + * Returns whether the specified key string matches the cell content at the given table location. + * + * @param row Row index of the cell. + * @param col Column index of the cell. + * @param key The search string. + * @param regex An optional regular expression that is applied to the cell content. Specify {@code null} to + * ignore. + * @param replacement An optional replacement string that is used in conjunction with {@code regex}. Specify + * {@code null} to ignore. + * @return {@code true} if a match is found, {@code false} otherwise. + */ + private boolean findMatch(int row, int col, String key, String regex, String replacement) { + boolean retVal = false; + if (key != null) { + final Object o = table.getModel().getValueAt(row, col); + if (o != null) { + final String curKey = key.toUpperCase(Locale.ROOT); + final String cellText; + if (regex != null && replacement != null) { + cellText = o.toString().replaceFirst(regex, replacement).toUpperCase(Locale.ROOT); + } else { + cellText = o.toString().toUpperCase(Locale.ROOT); + } + retVal = cellText.startsWith(curKey); + } + } + return retVal; + } + } } diff --git a/src/org/infinity/gui/ViewFrame.java b/src/org/infinity/gui/ViewFrame.java index 3431b14ec..54d441593 100644 --- a/src/org/infinity/gui/ViewFrame.java +++ b/src/org/infinity/gui/ViewFrame.java @@ -17,6 +17,7 @@ import org.infinity.resource.Viewable; import org.infinity.resource.ViewableContainer; import org.infinity.resource.graphics.BamResource; +import org.infinity.resource.key.BIFFResourceEntry; import org.infinity.resource.key.Keyfile; import org.infinity.resource.key.ResourceEntry; @@ -82,7 +83,13 @@ public void setViewable(Viewable viewable) { ResourceEntry entry = ((Resource) viewable).getResourceEntry(); // setTitle(entry.toString()); setIconImage(entry.getIcon().getImage()); - statusBar.setMessage(entry.getActualPath().toString()); + final String path; + if (entry instanceof BIFFResourceEntry) { + path = ((BIFFResourceEntry) entry).getActualPath(!entry.hasOverride()).toString(); + } else { + path = entry.getActualPath().toString(); + } + statusBar.setMessage(path); } else { setIconImage(Keyfile.ICON_STRUCT.getImage()); setTitle(((StructEntry) viewable).getName()); diff --git a/src/org/infinity/gui/ViewerUtil.java b/src/org/infinity/gui/ViewerUtil.java index 3aca585b1..0592dabab 100644 --- a/src/org/infinity/gui/ViewerUtil.java +++ b/src/org/infinity/gui/ViewerUtil.java @@ -209,7 +209,7 @@ public static JLabel makeBamPanel(ResourceRef iconRef, int frameNr) { return new JLabel("No " + iconRef.getName().toLowerCase(Locale.ENGLISH), SwingConstants.CENTER); } - public static JComponent makeBamPanel(ResourceRef iconRef, int animNr, int frameNr) { + public static JLabel makeBamPanel(ResourceRef iconRef, int animNr, int frameNr) { ResourceEntry iconEntry = ResourceFactory.getResourceEntry(iconRef.getResourceName()); if (iconEntry != null) { try { @@ -233,7 +233,7 @@ public static JComponent makeBamPanel(ResourceRef iconRef, int animNr, int frame return new JLabel("No " + iconRef.getName().toLowerCase(Locale.ENGLISH), SwingConstants.CENTER); } - public static JComponent makeCheckLabel(StructEntry entry, String yes) { + public static JLabel makeCheckLabel(StructEntry entry, String yes) { JLabel check = new JLabel(entry.getName()); if (entry.toString().equalsIgnoreCase(yes)) { check.setIcon(Icons.ICON_CHECK_16.getIcon()); @@ -296,7 +296,7 @@ public static JLabel makeImagePanel(ResourceRef imageRef, boolean searchExtraDir * * @return Editor for show list of the specified attributes */ - public static JPanel makeListPanel(String title, AbstractStruct struct, Class listClass) { + public static StructListPanel makeListPanel(String title, AbstractStruct struct, Class listClass) { return new StructListPanel(title, struct, listClass, null, null, null); } @@ -310,7 +310,7 @@ public static JPanel makeListPanel(String title, AbstractStruct struct, Class listClass, + public static StructListPanel makeListPanel(String title, AbstractStruct struct, Class listClass, String attrName) { return new StructListPanel(title, struct, listClass, getAttributeEntry(attrName), null, null); } @@ -326,7 +326,7 @@ public static JPanel makeListPanel(String title, AbstractStruct struct, Class listClass, + public static StructListPanel makeListPanel(String title, AbstractStruct struct, Class listClass, String attrName, ListCellRenderer renderer) { return new StructListPanel(title, struct, listClass, getAttributeEntry(attrName), renderer, null); } @@ -343,7 +343,7 @@ public static JPanel makeListPanel(String title, AbstractStruct struct, Class listClass, + public static StructListPanel makeListPanel(String title, AbstractStruct struct, Class listClass, String attrName, ListCellRenderer renderer, ListSelectionListener listener) { return new StructListPanel(title, struct, listClass, getAttributeEntry(attrName), renderer, listener); } @@ -358,7 +358,7 @@ public static JPanel makeListPanel(String title, AbstractStruct struct, Class listClass, + public static StructListPanel makeListPanel(String title, AbstractStruct struct, Class listClass, AttributeEntry attrEntry) { return new StructListPanel(title, struct, listClass, attrEntry, null, null); } @@ -374,7 +374,7 @@ public static JPanel makeListPanel(String title, AbstractStruct struct, Class listClass, + public static StructListPanel makeListPanel(String title, AbstractStruct struct, Class listClass, AttributeEntry attrEntry, ListCellRenderer renderer) { return new StructListPanel(title, struct, listClass, attrEntry, renderer, null); } @@ -391,7 +391,7 @@ public static JPanel makeListPanel(String title, AbstractStruct struct, Class listClass, + public static StructListPanel makeListPanel(String title, AbstractStruct struct, Class listClass, AttributeEntry attrEntry, ListCellRenderer renderer, ListSelectionListener listener) { return new StructListPanel(title, struct, listClass, attrEntry, renderer, listener); } @@ -551,8 +551,8 @@ public static interface AttributeEntry extends Function listClass; - private final JList list; - private final SimpleListModel listModel = new SimpleListModel<>(); + private final JList list; + private final SimpleListModel listModel = new SimpleListModel<>(); private final JButton bOpen = new JButton("View/Edit", Icons.ICON_ZOOM_16.getIcon()); private StructListPanel(String title, AbstractStruct struct, Class listClass, @@ -617,7 +617,7 @@ public void mouseClicked(MouseEvent e) { } /** Provides access to the list component of the panel. */ - public JList getList() { + public JList getList() { return list; } diff --git a/src/org/infinity/gui/converter/BamFilterOutputCombine.java b/src/org/infinity/gui/converter/BamFilterOutputCombine.java index 778db9442..d9647efbd 100644 --- a/src/org/infinity/gui/converter/BamFilterOutputCombine.java +++ b/src/org/infinity/gui/converter/BamFilterOutputCombine.java @@ -120,7 +120,7 @@ private boolean applyEffect(PseudoBamDecoder decoder) throws Exception { Graphics2D g = image.createGraphics(); try { - g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC)); + g.setComposite(AlphaComposite.Src); for (int i = 0; i < decoder.frameCount(); i++) { PseudoBamFrameEntry entry = decoder.getFrameInfo(i); int px = entry.getCenterX(); diff --git a/src/org/infinity/gui/converter/BamFilterTransformResize.java b/src/org/infinity/gui/converter/BamFilterTransformResize.java index 4ee0a93ec..9b070b8d9 100644 --- a/src/org/infinity/gui/converter/BamFilterTransformResize.java +++ b/src/org/infinity/gui/converter/BamFilterTransformResize.java @@ -557,7 +557,7 @@ private BufferedImage scaleNative(BufferedImage srcImage, double factorX, double // scaling image Graphics2D g = dstImage.createGraphics(); try { - g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC)); + g.setComposite(AlphaComposite.Src); BufferedImageOp op = new AffineTransformOp(AffineTransform.getScaleInstance(factorX, factorY), scaleType); g.drawImage(srcImage, op, 0, 0); } finally { diff --git a/src/org/infinity/gui/converter/ConvertToBam.java b/src/org/infinity/gui/converter/ConvertToBam.java index 192d6298f..c5514648f 100644 --- a/src/org/infinity/gui/converter/ConvertToBam.java +++ b/src/org/infinity/gui/converter/ConvertToBam.java @@ -2106,8 +2106,8 @@ private void updateQuickPreview(RenderCanvas target, int[] indices, boolean show int imgHeight = target.getImage().getHeight(null); Graphics2D g = (Graphics2D) target.getImage().getGraphics(); try { - g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC)); - g.setColor(new Color(0, true)); + g.setComposite(AlphaComposite.Src); + g.setColor(ColorConvert.TRANSPARENT_COLOR); g.fillRect(0, 0, imgWidth, imgHeight); if (indices != null && indices.length == 1 && indices[0] >= 0 && indices[0] < modelFrames.getSize()) { @@ -3466,8 +3466,8 @@ private synchronized void previewDisplay() { // clearing old content Graphics2D g = previewCanvas.createGraphics(); try { - g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC)); - g.setColor(new Color(0, true)); + g.setComposite(AlphaComposite.Src); + g.setColor(ColorConvert.TRANSPARENT_COLOR); g.fillRect(0, 0, previewCanvas.getWidth(), previewCanvas.getHeight()); } finally { g.dispose(); @@ -3481,8 +3481,8 @@ private synchronized void previewDisplay() { g = (Graphics2D) rcPreview.getImage().getGraphics(); try { // clearing old content - g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC)); - g.setColor(new Color(0, true)); + g.setComposite(AlphaComposite.Src); + g.setColor(ColorConvert.TRANSPARENT_COLOR); g.fillRect(0, 0, rcPreview.getImage().getWidth(null), rcPreview.getImage().getHeight(null)); // rendering frame @@ -3680,8 +3680,8 @@ private void filterDisplay(PseudoBamFrameEntry entry) { Graphics2D g = (Graphics2D) rcFiltersPreview.getImage().getGraphics(); try { // clearing old content - g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC)); - g.setColor(new Color(0, true)); + g.setComposite(AlphaComposite.Src); + g.setColor(ColorConvert.TRANSPARENT_COLOR); g.fillRect(0, 0, canvasWidth, canvasHeight); // rendering frame @@ -3878,8 +3878,8 @@ private DxtEncoder.DxtType getAutoDxtType() { for (int i = 0; i < bamDecoder.frameCount(); i++) { Graphics2D g = canvas.createGraphics(); try { - g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC)); - g.setColor(new Color(0, true)); + g.setComposite(AlphaComposite.Src); + g.setColor(ColorConvert.TRANSPARENT_COLOR); g.fillRect(0, 0, canvas.getWidth(), canvas.getHeight()); } finally { g.dispose(); diff --git a/src/org/infinity/gui/converter/ConvertToMos.java b/src/org/infinity/gui/converter/ConvertToMos.java index cb92a15ad..921b9e900 100644 --- a/src/org/infinity/gui/converter/ConvertToMos.java +++ b/src/org/infinity/gui/converter/ConvertToMos.java @@ -5,7 +5,6 @@ package org.infinity.gui.converter; import java.awt.AlphaComposite; -import java.awt.Color; import java.awt.Component; import java.awt.Dimension; import java.awt.Graphics2D; @@ -24,6 +23,7 @@ import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.io.OutputStream; +import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; import java.util.List; @@ -467,14 +467,14 @@ private static boolean createPvrzPages(Path path, BufferedImage img, DxtEncoder. BufferedImage texture = ColorConvert.createCompatibleImage(tw, th, true); Graphics2D g = texture.createGraphics(); g.setComposite(AlphaComposite.Src); - g.setColor(new Color(0, true)); + g.setColor(ColorConvert.TRANSPARENT_COLOR); g.fillRect(0, 0, texture.getWidth(), texture.getHeight()); for (final MosEntry entry : entryList) { if (entry.page == i) { int sx = entry.dstLocation.x, sy = entry.dstLocation.y; int dx = entry.srcLocation.x, dy = entry.srcLocation.y; int w = entry.width, h = entry.height; - g.clearRect(dx, dy, w, h); + g.fillRect(dx, dy, w, h); g.drawImage(img, dx, dy, dx + w, dy + h, sx, sy, sx + w, sy + h, null); } } @@ -925,6 +925,11 @@ private String getImageFileName(Path path) { fc.addChoosableFileFilter(filter); } fc.setFileFilter(filters[0]); + if (Files.isRegularFile(path)) { + fc.setSelectedFile(path.toFile()); + } else { + fc.setCurrentDirectory(path.getParent().toFile()); + } int ret = fc.showOpenDialog(this); if (ret == JFileChooser.APPROVE_OPTION) { return fc.getSelectedFile().toString(); diff --git a/src/org/infinity/gui/converter/ConvertToPvrz.java b/src/org/infinity/gui/converter/ConvertToPvrz.java index 3ba2161c3..e8bb6210e 100644 --- a/src/org/infinity/gui/converter/ConvertToPvrz.java +++ b/src/org/infinity/gui/converter/ConvertToPvrz.java @@ -237,7 +237,7 @@ public List doInBackground() { fc.setDialogTitle("Choose target directory"); fc.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY); if (!tfTargetDir.getText().isEmpty()) { - fc.setSelectedFile(FileManager.resolve(tfTargetDir.getText()).toFile()); + fc.setCurrentDirectory(FileManager.resolve(tfTargetDir.getText()).toFile()); } int ret = fc.showOpenDialog(this); if (ret == JFileChooser.APPROVE_OPTION) { diff --git a/src/org/infinity/gui/converter/ConvertToTis.java b/src/org/infinity/gui/converter/ConvertToTis.java index 399cc3be5..49f75dd93 100644 --- a/src/org/infinity/gui/converter/ConvertToTis.java +++ b/src/org/infinity/gui/converter/ConvertToTis.java @@ -5,7 +5,6 @@ package org.infinity.gui.converter; import java.awt.AlphaComposite; -import java.awt.Color; import java.awt.Component; import java.awt.Dimension; import java.awt.Graphics2D; @@ -23,12 +22,14 @@ import java.awt.image.DataBufferInt; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; +import java.io.File; import java.io.OutputStream; import java.nio.file.Path; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.List; +import java.util.Objects; import java.util.Vector; import java.util.regex.Pattern; @@ -506,16 +507,15 @@ public static boolean createPvrzPages(String tisFileName, BufferedImage srcImg, int h = packer.getBinHeight() * 64; BufferedImage texture = ColorConvert.createCompatibleImage(w, h, true); Graphics2D g = texture.createGraphics(); - g.setBackground(new Color(0, true)); g.setComposite(AlphaComposite.Src); - g.setColor(new Color(0, true)); + g.setColor(ColorConvert.TRANSPARENT_COLOR); g.fillRect(0, 0, texture.getWidth(), texture.getHeight()); int tw = srcImg.getWidth() / 64; for (final TileEntry entry : entryList) { if (entry.page == pageIdx) { int sx = (entry.tileIndex % tw) * 64, sy = (entry.tileIndex / tw) * 64; int dx = entry.x, dy = entry.y; - g.clearRect(dx, dy, 64, 64); + g.fillRect(dx, dy, 64, 64); g.drawImage(srcImg, dx, dy, dx + 64, dy + 64, sx, sy, sx + 64, sy + 64, null); } } @@ -614,7 +614,13 @@ public List doInBackground() { } fc.setFileFilter(filters[0]); if (!tfInput.getText().isEmpty()) { - fc.setSelectedFile(FileManager.resolve(tfInput.getText()).toFile()); + final File file = FileManager.resolve(tfInput.getText()).toFile(); + if (file.isFile()) { + fc.setCurrentDirectory(file.getParentFile()); + fc.setSelectedFile(file); + } else { + fc.setCurrentDirectory(file.getParentFile()); + } } int ret = fc.showOpenDialog(this); if (ret == JFileChooser.APPROVE_OPTION) { @@ -642,6 +648,7 @@ public List doInBackground() { fileName = createValidTisName(tfInput.getText(), getTisVersion()); } } + fc.setCurrentDirectory(FileManager.resolve(fileName).toFile()); fc.setSelectedFile(FileManager.resolve(fileName).toFile()); int ret = fc.showSaveDialog(this); while (ret == JFileChooser.APPROVE_OPTION) { @@ -1090,5 +1097,38 @@ public TileEntry(int index, int page, int x, int y) { this.x = x; this.y = y; } + + public TileEntry(TileEntry tileEntry) { + Objects.requireNonNull(tileEntry); + this.tileIndex = tileEntry.tileIndex; + this.page = tileEntry.page; + this.x = tileEntry.x; + this.y = tileEntry.y; + } + + @Override + public int hashCode() { + return Objects.hash(page, tileIndex, x, y); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + TileEntry other = (TileEntry)obj; + return page == other.page && tileIndex == other.tileIndex && x == other.x && y == other.y; + } + + @Override + public String toString() { + return "TileEntry [tileIndex=" + tileIndex + ", page=" + page + ", x=" + x + ", y=" + y + "]"; + } } } diff --git a/src/org/infinity/gui/layeritem/AnimatedLayerItem.java b/src/org/infinity/gui/layeritem/AnimatedLayerItem.java index 58da4fcec..359789780 100644 --- a/src/org/infinity/gui/layeritem/AnimatedLayerItem.java +++ b/src/org/infinity/gui/layeritem/AnimatedLayerItem.java @@ -33,8 +33,6 @@ */ public class AnimatedLayerItem extends AbstractLayerItem implements LayerItemListener, ActionListener, PropertyChangeListener { - private static final Color TRANSPARENT_COLOR = new Color(0, true); - private final FrameInfo[] frameInfos = { new FrameInfo(), new FrameInfo() }; private BasicAnimationProvider animation; @@ -484,8 +482,8 @@ private synchronized void updateCanvas() { Graphics2D g2 = (Graphics2D) rcCanvas.getImage().getGraphics(); try { - g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC)); - g2.setColor(TRANSPARENT_COLOR); + g2.setComposite(AlphaComposite.Src); + g2.setColor(ColorConvert.TRANSPARENT_COLOR); g2.fillRect(0, 0, rcCanvas.getImage().getWidth(null), rcCanvas.getImage().getHeight(null)); // drawing animation graphics diff --git a/src/org/infinity/gui/layeritem/IconLayerItem.java b/src/org/infinity/gui/layeritem/IconLayerItem.java index 37362d5cf..ca7f7373b 100644 --- a/src/org/infinity/gui/layeritem/IconLayerItem.java +++ b/src/org/infinity/gui/layeritem/IconLayerItem.java @@ -311,7 +311,7 @@ private void setCurrentImage(ItemState state) { } rcCanvas.setImage(getImage(state)); } else { - label.setBackground(new Color(0, true)); + label.setBackground(ColorConvert.TRANSPARENT_COLOR); rcCanvas.setImage(null); } } diff --git a/src/org/infinity/gui/menu/FileMenu.java b/src/org/infinity/gui/menu/FileMenu.java index 0df287e61..5b7403984 100644 --- a/src/org/infinity/gui/menu/FileMenu.java +++ b/src/org/infinity/gui/menu/FileMenu.java @@ -23,6 +23,7 @@ import org.infinity.resource.Resource; import org.infinity.resource.ResourceFactory; import org.infinity.resource.StructureFactory; +import org.infinity.resource.key.BIFFResourceEntry; import org.infinity.resource.key.FileResourceEntry; import org.infinity.resource.key.ResourceEntry; @@ -78,6 +79,7 @@ public class FileMenu extends JMenu implements BrowserSubMenu, ActionListener { private final JMenu newFileMenu; private final JMenuItem fileOpenNew; + private final JMenuItem fileOpenBiffedNew; private final JMenuItem fileReference; private final JMenuItem fileExport; private final JMenuItem fileAddCopy; @@ -98,6 +100,10 @@ public FileMenu(BrowserMenuBar parent) { fileOpenNew = BrowserMenuBar.makeMenuItem("Open in New Window", KeyEvent.VK_W, Icons.ICON_OPEN_16.getIcon(), -1, this); fileOpenNew.setEnabled(false); add(fileOpenNew); + fileOpenBiffedNew = BrowserMenuBar.makeMenuItem("Open Biffed Resource in New Window", KeyEvent.VK_W, + Icons.ICON_OPEN_16.getIcon(), -1, this); + fileOpenBiffedNew.setEnabled(false); + add(fileOpenBiffedNew); fileReference = BrowserMenuBar.makeMenuItem("Find references...", KeyEvent.VK_F, Icons.ICON_FIND_16.getIcon(), -1, this); fileReference.setEnabled(false); add(fileReference); @@ -136,9 +142,17 @@ public void gameLoaded() { } public void resourceEntrySelected(ResourceEntry entry) { + boolean biffEnable = entry != null && + (!BrowserMenuBar.getInstance().getOptions().ignoreOverrides() || + BrowserMenuBar.getInstance().getOptions().getOverrideMode() == OverrideMode.InOverride) && + entry.hasOverride(); + fileOpenBiffedNew.setEnabled(biffEnable); + fileOpenNew.setEnabled(entry != null); + Class cls = ResourceFactory.getResourceType(entry); fileReference.setEnabled(cls != null && Referenceable.class.isAssignableFrom(cls)); + fileExport.setEnabled(entry != null); fileAddCopy.setEnabled(entry != null); fileRename.setEnabled(entry instanceof FileResourceEntry); @@ -158,6 +172,20 @@ public void actionPerformed(ActionEvent event) { if (res != null) { new ViewFrame(NearInfinity.getInstance(), res); } + } else if (event.getSource() == fileOpenBiffedNew) { + final ResourceEntry node = NearInfinity.getInstance().getResourceTree().getSelected(); + try { + final BIFFResourceEntry biffedNode = + new BIFFResourceEntry(ResourceFactory.getKeyfile().getResourceEntry(node.getResourceName()), false); + final Resource res = ResourceFactory.getResource(biffedNode); + if (res != null) { + new ViewFrame(NearInfinity.getInstance(), res); + } + } catch (NullPointerException e) { + System.err.println("Does not exist in BIFF: " + node); + JOptionPane.showMessageDialog(NearInfinity.getInstance(), + "Does not exist in BIFF: " + node, "Error", JOptionPane.ERROR_MESSAGE); + } } else if (event.getSource() == fileReference) { Resource res = ResourceFactory.getResource(NearInfinity.getInstance().getResourceTree().getSelected()); if (res instanceof Referenceable) { diff --git a/src/org/infinity/gui/menu/OptionsMenuItem.java b/src/org/infinity/gui/menu/OptionsMenuItem.java index 9a31ef589..2a5b4e237 100644 --- a/src/org/infinity/gui/menu/OptionsMenuItem.java +++ b/src/org/infinity/gui/menu/OptionsMenuItem.java @@ -111,6 +111,18 @@ public String toString() { new ColorScheme(InfinityTextArea.Scheme.BCS.getLabel(), InfinityTextArea.Scheme.BCS.getScheme()) ); + /** + * Available color schemes for highlighted DLG script trigger/action format (title and scheme definition path). + * Only "light" color schemes are usable. + */ + private static final List DLG_COLOR_SCHEME = Arrays.asList( + new ColorScheme(InfinityTextArea.Scheme.DEFAULT.getLabel(), InfinityTextArea.Scheme.DEFAULT.getScheme()), + new ColorScheme(InfinityTextArea.Scheme.ECLIPSE.getLabel(), InfinityTextArea.Scheme.ECLIPSE.getScheme()), + new ColorScheme(InfinityTextArea.Scheme.IDEA.getLabel(), InfinityTextArea.Scheme.IDEA.getScheme()), + new ColorScheme(InfinityTextArea.Scheme.VS.getLabel(), InfinityTextArea.Scheme.VS.getScheme()), + new ColorScheme(InfinityTextArea.Scheme.BCS.getLabel(), InfinityTextArea.Scheme.BCS.getScheme()) + ); + /** Available color schemes for remaining highlighted formats (scheme, title, description). */ private static final List COLOR_SCHEME = BCS_COLOR_SCHEME.subList(0, BCS_COLOR_SCHEME.size() - 1); @@ -181,6 +193,8 @@ public String toString() { public static final String OPTION_INI_COLORSCHEME = "IniColorScheme"; public static final String OPTION_LUA_SYNTAXHIGHLIGHTING = "LuaSyntaxHighlighting"; public static final String OPTION_LUA_COLORSCHEME = "LuaColorScheme"; + public static final String OPTION_MENU_SYNTAXHIGHLIGHTING = "MenuSyntaxHighlighting"; + public static final String OPTION_MENU_COLORSCHEME = "MenuColorScheme"; public static final String OPTION_SQL_SYNTAXHIGHLIGHTING = "SqlSyntaxHighlighting"; public static final String OPTION_SQL_COLORSCHEME = "SqlColorScheme"; public static final String OPTION_TLK_SYNTAXHIGHLIGHTING = "TlkSyntaxHighlighting"; @@ -188,6 +202,8 @@ public String toString() { public static final String OPTION_WEIDU_SYNTAXHIGHLIGHTING = "WeiDUSyntaxHighlighting"; public static final String OPTION_WEIDU_COLORSCHEME = "WeiDUColorScheme"; + public static final String OPTION_DLG_COLORSCHEME = "DlgColorScheme"; + public static final String OPTION_DLG_SYNTAXHIGHLIGHTING = "DlgSyntaxHighlighting"; public static final String OPTION_SHOWICONS = "DlgShowIcons"; public static final String OPTION_SORT_STATES_BY_WEIGHT = "DlgSortStatesByWeight"; public static final String OPTION_ALWAYS_SHOW_STATE_0 = "DlgAlwaysShowState0"; @@ -227,6 +243,11 @@ public static List getBcsColorSchemes() { return Collections.unmodifiableList(BCS_COLOR_SCHEME); } + /** Returns a list of all available color schemes for script trigger/action format in DLG tree viewer. */ + public static List getDlgColorSchemes() { + return Collections.unmodifiableList(DLG_COLOR_SCHEME); + } + /** Returns a list of all available color schemes for text-based formats except BCS. */ public static List getColorSchemes() { return Collections.unmodifiableList(COLOR_SCHEME); @@ -611,6 +632,11 @@ public boolean getLuaSyntaxHighlightingEnabled() { return AppOption.LUA_SYNTAX_HIGHLIGHTING.getBoolValue(); } + /** Returns state of "Enable Syntax Highlighting for MENU" */ + public boolean getMenuSyntaxHighlightingEnabled() { + return AppOption.MENU_SYNTAX_HIGHLIGHTING.getBoolValue(); + } + /** Returns state of "Enable Syntax Highlighting for SQL" */ public boolean getSqlSyntaxHighlightingEnabled() { return AppOption.SQL_SYNTAX_HIGHLIGHTING.getBoolValue(); @@ -626,6 +652,11 @@ public boolean getWeiDUSyntaxHighlightingEnabled() { return AppOption.WEIDU_SYNTAX_HIGHLIGHTING.getBoolValue(); } + /** Returns state of "DLG Tree Viewer: Enable Syntax Highlighting" */ + public boolean getDlgSyntaxHighlightingEnabled() { + return AppOption.DLG_SYNTAX_HIGHLIGHTING.getBoolValue(); + } + /** Returns state of "BCS: Enable Code Folding" */ public boolean getBcsCodeFoldingEnabled() { return AppOption.BCS_CODE_FOLDING.getBoolValue(); @@ -656,6 +687,11 @@ public String getLuaColorScheme() { return COLOR_SCHEME.get(idx).getPath(); } + public String getMenuColorScheme() { + int idx = AppOption.MENU_COLOR_SCHEME.getIntValue(); + return COLOR_SCHEME.get(idx).getPath(); + } + public String getSqlColorScheme() { int idx = AppOption.SQL_COLOR_SCHEME.getIntValue(); return COLOR_SCHEME.get(idx).getPath(); @@ -671,6 +707,11 @@ public String getWeiDUColorScheme() { return COLOR_SCHEME.get(idx).getPath(); } + public String getDlgColorScheme() { + int idx = AppOption.DLG_COLOR_SCHEME.getIntValue(); + return DLG_COLOR_SCHEME.get(idx).getPath(); + } + public AutoAlign2da getAutoAlign2da() { int idx = AppOption.AUTO_ALIGN_2DA.getIntValue(); if (idx >= 0 && idx < AutoAlign2da.values().length) { diff --git a/src/org/infinity/resource/Effect.java b/src/org/infinity/resource/Effect.java index be28ea46e..8c3010779 100644 --- a/src/org/infinity/resource/Effect.java +++ b/src/org/infinity/resource/Effect.java @@ -9,7 +9,9 @@ import java.util.ArrayList; import java.util.List; +import org.infinity.datatype.DecNumber; import org.infinity.datatype.EffectType; +import org.infinity.resource.effects.BaseOpcode; import org.infinity.util.io.StreamUtils; public final class Effect extends AbstractStruct implements AddRemovable { @@ -18,6 +20,7 @@ public final class Effect extends AbstractStruct implements AddRemovable { public Effect() throws Exception { super(null, EFFECT, StreamUtils.getByteBuffer(48), 0); + ((DecNumber) getAttribute(BaseOpcode.EFFECT_PROBABILITY_1)).setValue(100); } public Effect(AbstractStruct superStruct, ByteBuffer buffer, int offset, int number) throws Exception { diff --git a/src/org/infinity/resource/Effect2.java b/src/org/infinity/resource/Effect2.java index 03b532204..2f67f8719 100644 --- a/src/org/infinity/resource/Effect2.java +++ b/src/org/infinity/resource/Effect2.java @@ -19,6 +19,7 @@ import org.infinity.datatype.SecTypeBitmap; import org.infinity.datatype.TextString; import org.infinity.datatype.Unknown; +import org.infinity.resource.effects.BaseOpcode; import org.infinity.util.IdsMapEntry; import org.infinity.util.io.StreamUtils; @@ -124,6 +125,7 @@ public static int readCommon(List list, ByteBuffer buffer, int offs public Effect2() throws Exception { super(null, EFFECT, StreamUtils.getByteBuffer(264), 0); + ((DecNumber) getAttribute(BaseOpcode.EFFECT_PROBABILITY_1)).setValue(100); } public Effect2(AbstractStruct superStruct, ByteBuffer buffer, int offset, int number) throws Exception { diff --git a/src/org/infinity/resource/StructureFactory.java b/src/org/infinity/resource/StructureFactory.java index 668df8b88..104ecc815 100644 --- a/src/org/infinity/resource/StructureFactory.java +++ b/src/org/infinity/resource/StructureFactory.java @@ -410,7 +410,10 @@ private ResourceStructure createEFF() { structEff.add(ResourceStructure.ID_STRING, 4, "V2.0"); // Version structEff.add(ResourceStructure.ID_STRING, 4, "EFF "); // Signature 2 structEff.add(ResourceStructure.ID_STRING, 4, "V2.0"); // Version 2 - structEff.add(ResourceStructure.ID_BUFFER, 256); // block of zeros + + final byte[] buf = new byte[256]; + buf[0x1c] = 100; // Probability1 = 100 + structEff.add(ResourceStructure.ID_BUFFER, 256, buf); // EFF data block return structEff; } diff --git a/src/org/infinity/resource/are/Animation.java b/src/org/infinity/resource/are/Animation.java index 7e0866007..9c71a682c 100644 --- a/src/org/infinity/resource/are/Animation.java +++ b/src/org/infinity/resource/are/Animation.java @@ -41,6 +41,11 @@ public final class Animation extends AbstractStruct implements AddRemovable { "Draw as background", "Play all frames", "Recolored by palette", "Mirror Y axis", "Don't remove in combat", "EE: Use WBM", "EE: Draw stenciled", "EE: Use PVRZ" }; + public static final String[] FLAGS_PSTEE_ARRAY = { "Not shown", "Is shown", "Blended", "Not light source", + "Partial animation", "Synchronized draw", "Random start", "Not covered by wall", "Disable on slow machines", + "Alt. blending mode", "Play all frames", "Recolored by palette", "Mirror Y axis", "Don't remove in combat", + "unused", "unused", "Use PVRZ (non-functional)", "Cover animations" }; + Animation() throws Exception { super(null, ARE_ANIMATION, StreamUtils.getByteBuffer(76), 0); } @@ -71,7 +76,11 @@ public int read(ByteBuffer buffer, int offset) throws Exception { } addField(new DecNumber(buffer, offset + 48, 2, ARE_ANIMATION_ANIMATION_INDEX)); addField(new DecNumber(buffer, offset + 50, 2, ARE_ANIMATION_FRAME_INDEX)); - addField(new Flag(buffer, offset + 52, 4, ARE_ANIMATION_APPEARANCE, FLAGS_ARRAY)); + if ((Profile.getGame() == Profile.Game.PSTEE)) { + addField(new Flag(buffer, offset + 52, 4, ARE_ANIMATION_APPEARANCE, FLAGS_PSTEE_ARRAY)); + } else { + addField(new Flag(buffer, offset + 52, 4, ARE_ANIMATION_APPEARANCE, FLAGS_ARRAY)); + } addField(new DecNumber(buffer, offset + 56, 2, ARE_ANIMATION_LOCATION_Z)); addField(new DecNumber(buffer, offset + 58, 2, ARE_ANIMATION_TRANSLUCENCY)); addField(new DecNumber(buffer, offset + 60, 2, ARE_ANIMATION_START_RANGE)); diff --git a/src/org/infinity/resource/are/Container.java b/src/org/infinity/resource/are/Container.java index e4a53e8e9..11a8d80c3 100644 --- a/src/org/infinity/resource/are/Container.java +++ b/src/org/infinity/resource/are/Container.java @@ -5,6 +5,10 @@ package org.infinity.resource.are; import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; import javax.swing.JComponent; @@ -21,8 +25,12 @@ import org.infinity.resource.AddRemovable; import org.infinity.resource.HasChildStructs; import org.infinity.resource.HasViewerTabs; +import org.infinity.resource.Profile; +import org.infinity.resource.ResourceFactory; import org.infinity.resource.StructEntry; import org.infinity.resource.vertex.Vertex; +import org.infinity.util.Table2da; +import org.infinity.util.Table2daCache; import org.infinity.util.io.StreamUtils; public final class Container extends AbstractStruct @@ -56,7 +64,7 @@ public final class Container extends AbstractStruct public static final String ARE_CONTAINER_BREAK_DIFFICULTY = "Break difficulty"; public static final String ARE_CONTAINER_LOCKPICK_STRING = "Lockpick string"; - public static final String[] TYPE_ARRAY = { "", "Bag", "Chest", "Drawer", "Pile", "Table", "Shelf", "Altar", + public static final String[] TYPE_ARRAY = { "Default", "Bag", "Chest", "Drawer", "Pile", "Table", "Shelf", "Altar", "Non-visible", "Spellbook", "Body", "Barrel", "Crate" }; public static final String[] FLAG_ARRAY = { "No flags set", "Locked", "Disable if no owner", "Magical lock", @@ -178,7 +186,7 @@ public int read(ByteBuffer buffer, int offset) throws Exception { addField(new TextString(buffer, offset, 32, ARE_CONTAINER_NAME)); addField(new DecNumber(buffer, offset + 32, 2, ARE_CONTAINER_LOCATION_X)); addField(new DecNumber(buffer, offset + 34, 2, ARE_CONTAINER_LOCATION_Y)); - addField(new Bitmap(buffer, offset + 36, 2, ARE_CONTAINER_TYPE, TYPE_ARRAY)); + addField(new Bitmap(buffer, offset + 36, 2, ARE_CONTAINER_TYPE, getContainerTypes())); addField(new DecNumber(buffer, offset + 38, 2, ARE_CONTAINER_LOCK_DIFFICULTY)); addField(new Flag(buffer, offset + 40, 4, ARE_CONTAINER_FLAGS, FLAG_ARRAY)); addField(new DecNumber(buffer, offset + 44, 2, ARE_CONTAINER_TRAP_DETECTION_DIFFICULTY)); @@ -204,4 +212,31 @@ public int read(ByteBuffer buffer, int offset) throws Exception { addField(new Unknown(buffer, offset + 136, 56)); return offset + 192; } + + /** Generates a string array of available container types dynamically. */ + private static String[] getContainerTypes() { + String[] retVal = TYPE_ARRAY; + final String container_file = "containr.2da"; + if (Profile.isEnhancedEdition() && ResourceFactory.resourceExists(container_file)) { + final List types = new ArrayList<>(); + final Table2da table = Table2daCache.get(container_file); + if (table != null) { + for (int row = 0, count = table.getRowCount(); row < count; row++) { + final String name = table.get(row, 0); + if (name != null && !name.equals(table.getDefaultValue())) { + final String nameNormalized = Arrays.stream(name.replace('_', ' ').split("\\s+")) + .map(word -> word.substring(0, 1).toUpperCase() + word.substring(1).toLowerCase()) + .collect(Collectors.joining(" ")); + types.add(nameNormalized); + } else { + types.add(TYPE_ARRAY[0]); + } + } + } + if (!table.isEmpty()) { + retVal = types.toArray(new String[0]); + } + } + return retVal; + } } diff --git a/src/org/infinity/resource/are/RestSpawn.java b/src/org/infinity/resource/are/RestSpawn.java index 868403803..fd7eb4dd0 100644 --- a/src/org/infinity/resource/are/RestSpawn.java +++ b/src/org/infinity/resource/are/RestSpawn.java @@ -13,22 +13,26 @@ import org.infinity.datatype.TextString; import org.infinity.datatype.Unknown; import org.infinity.resource.AbstractStruct; +import org.infinity.resource.Profile; public final class RestSpawn extends AbstractStruct { // ARE/Rest Encounters-specific field labels - public static final String ARE_RESTSPAWN = "Rest encounters"; - public static final String ARE_RESTSPAWN_NAME = "Name"; - public static final String ARE_RESTSPAWN_CREATURE_STRING_FMT = "Creature %d string"; - public static final String ARE_RESTSPAWN_CREATURE_FMT = "Creature %d"; - public static final String ARE_RESTSPAWN_NUM_CREATURES = "# creatures"; - public static final String ARE_RESTSPAWN_ENCOUNTER_DIFFICULTY = "Encounter difficulty"; - public static final String ARE_RESTSPAWN_DURATION = "Creature duration"; - public static final String ARE_RESTSPAWN_WANDER_DISTANCE = "Creature wander distance"; - public static final String ARE_RESTSPAWN_FOLLOW_DISTANCE = "Creature follow distance"; - public static final String ARE_RESTSPAWN_MAX_CREATURES = "Maximum spawned creatures"; - public static final String ARE_RESTSPAWN_ACTIVE = "Is active?"; - public static final String ARE_RESTSPAWN_PROBABILITY_DAY = "Probability (day)"; - public static final String ARE_RESTSPAWN_PROBABILITY_NIGHT = "Probability (night)"; + public static final String ARE_RESTSPAWN = "Rest encounters"; + public static final String ARE_RESTSPAWN_NAME = "Name"; + public static final String ARE_RESTSPAWN_CREATURE_STRING_FMT = "Creature %d string"; + public static final String ARE_RESTSPAWN_CREATURE_FMT = "Creature %d"; + public static final String ARE_RESTSPAWN_NUM_CREATURES = "# creatures"; + public static final String ARE_RESTSPAWN_ENCOUNTER_DIFFICULTY = "Encounter difficulty"; + public static final String ARE_RESTSPAWN_DURATION = "Creature duration"; + public static final String ARE_RESTSPAWN_WANDER_DISTANCE = "Creature wander distance"; + public static final String ARE_RESTSPAWN_FOLLOW_DISTANCE = "Creature follow distance"; + public static final String ARE_RESTSPAWN_MAX_CREATURES = "Maximum spawned creatures"; + public static final String ARE_RESTSPAWN_NUM_SPAWNS = "# spawns (base)"; + public static final String ARE_RESTSPAWN_ACTIVE = "Is active?"; + public static final String ARE_RESTSPAWN_PROBABILITY_DAY = "Probability (day)"; + public static final String ARE_RESTSPAWN_PROBABILITY_NIGHT = "Probability (night)"; + public static final String ARE_RESTSPAWN_PROBABILITY_DAY_HOUR = "Hourly probability (day)"; + public static final String ARE_RESTSPAWN_PROBABILITY_NIGHT_HOUR = "Hourly probability (night)"; RestSpawn(AbstractStruct superStruct, ByteBuffer buffer, int offset) throws Exception { super(superStruct, ARE_RESTSPAWN, buffer, offset); @@ -36,6 +40,8 @@ public final class RestSpawn extends AbstractStruct { @Override public int read(ByteBuffer buffer, int offset) throws Exception { + final boolean isIWD = (Profile.getEngine() == Profile.Engine.IWD || Profile.getEngine() == Profile.Engine.IWD2); + addField(new TextString(buffer, offset, 32, ARE_RESTSPAWN_NAME)); for (int i = 0; i < 10; i++) { addField(new StringRef(buffer, offset + 32 + (i * 4), String.format(ARE_RESTSPAWN_CREATURE_STRING_FMT, i + 1))); @@ -44,14 +50,27 @@ public int read(ByteBuffer buffer, int offset) throws Exception { addField(new SpawnResourceRef(buffer, offset + 72 + i * 8, String.format(ARE_RESTSPAWN_CREATURE_FMT, i + 1))); } addField(new DecNumber(buffer, offset + 152, 2, ARE_RESTSPAWN_NUM_CREATURES)); - addField(new DecNumber(buffer, offset + 154, 2, ARE_RESTSPAWN_ENCOUNTER_DIFFICULTY)); + if (isIWD) { + addField(new DecNumber(buffer, offset + 154, 2, COMMON_UNUSED)); + } else { + addField(new DecNumber(buffer, offset + 154, 2, ARE_RESTSPAWN_ENCOUNTER_DIFFICULTY)); + } addField(new DecNumber(buffer, offset + 156, 4, ARE_RESTSPAWN_DURATION)); addField(new DecNumber(buffer, offset + 160, 2, ARE_RESTSPAWN_WANDER_DISTANCE)); addField(new DecNumber(buffer, offset + 162, 2, ARE_RESTSPAWN_FOLLOW_DISTANCE)); - addField(new DecNumber(buffer, offset + 164, 2, ARE_RESTSPAWN_MAX_CREATURES)); + if (isIWD) { + addField(new DecNumber(buffer, offset + 164, 2, ARE_RESTSPAWN_NUM_SPAWNS)); + } else { + addField(new DecNumber(buffer, offset + 164, 2, ARE_RESTSPAWN_MAX_CREATURES)); + } addField(new Bitmap(buffer, offset + 166, 2, ARE_RESTSPAWN_ACTIVE, OPTION_NOYES)); - addField(new DecNumber(buffer, offset + 168, 2, ARE_RESTSPAWN_PROBABILITY_DAY)); - addField(new DecNumber(buffer, offset + 170, 2, ARE_RESTSPAWN_PROBABILITY_NIGHT)); + if (isIWD) { + addField(new DecNumber(buffer, offset + 168, 2, ARE_RESTSPAWN_PROBABILITY_DAY)); + addField(new DecNumber(buffer, offset + 170, 2, ARE_RESTSPAWN_PROBABILITY_NIGHT)); + } else { + addField(new DecNumber(buffer, offset + 168, 2, ARE_RESTSPAWN_PROBABILITY_DAY_HOUR)); + addField(new DecNumber(buffer, offset + 170, 2, ARE_RESTSPAWN_PROBABILITY_NIGHT_HOUR)); + } addField(new Unknown(buffer, offset + 172, 56)); return offset + 228; } diff --git a/src/org/infinity/resource/are/viewer/ActorAnimationProvider.java b/src/org/infinity/resource/are/viewer/ActorAnimationProvider.java index eb3965f76..ca0f4d4fd 100644 --- a/src/org/infinity/resource/are/viewer/ActorAnimationProvider.java +++ b/src/org/infinity/resource/are/viewer/ActorAnimationProvider.java @@ -5,7 +5,6 @@ package org.infinity.resource.are.viewer; import java.awt.AlphaComposite; -import java.awt.Color; import java.awt.Graphics2D; import java.awt.Point; import java.awt.Rectangle; @@ -23,8 +22,6 @@ * Implements functionality for properly displaying actor sprites. */ public class ActorAnimationProvider extends AbstractAnimationProvider { - private static final Color TRANSPARENT_COLOR = new Color(0, true); - private SpriteDecoder decoder; private SpriteBamControl control; private boolean isLooping; @@ -265,7 +262,7 @@ protected synchronized void updateGraphics() { try { // clearing old content g.setComposite(AlphaComposite.Src); - g.setColor(TRANSPARENT_COLOR); + g.setColor(ColorConvert.TRANSPARENT_COLOR); g.fillRect(0, 0, image.getWidth(), image.getHeight()); } finally { g.dispose(); @@ -312,7 +309,7 @@ protected synchronized void updateGraphics() { g = image.createGraphics(); try { g.setComposite(AlphaComposite.Src); - g.setColor(TRANSPARENT_COLOR); + g.setColor(ColorConvert.TRANSPARENT_COLOR); g.fillRect(0, 0, image.getWidth(), image.getHeight()); } finally { g.dispose(); diff --git a/src/org/infinity/resource/are/viewer/AreaViewer.java b/src/org/infinity/resource/are/viewer/AreaViewer.java index b21d8a1fb..c868b2d9c 100644 --- a/src/org/infinity/resource/are/viewer/AreaViewer.java +++ b/src/org/infinity/resource/are/viewer/AreaViewer.java @@ -27,6 +27,7 @@ import java.awt.event.MouseMotionListener; import java.awt.event.MouseWheelEvent; import java.awt.event.MouseWheelListener; +import java.awt.geom.AffineTransform; import java.awt.image.BufferedImage; import java.awt.image.VolatileImage; import java.beans.PropertyChangeEvent; @@ -127,7 +128,8 @@ public class AreaViewer extends ChildFrame { private static final String LABEL_DRAW_CLOSED = "Draw closed"; private static final String LABEL_DRAW_OVERLAYS = "Enable overlays"; private static final String LABEL_ANIMATE_OVERLAYS = "Animate overlays"; - private static final String LABEL_DRAW_GRID = "Show grid"; + private static final String LABEL_DRAW_TILE_GRID = "Show tile grid"; + private static final String LABEL_DRAW_CELL_GRID = "Show cell grid"; private final Listeners listeners; private final Map map; @@ -160,7 +162,8 @@ public class AreaViewer extends ChildFrame { private JCheckBox cbDrawClosed; private JCheckBox cbDrawOverlays; private JCheckBox cbAnimateOverlays; - private JCheckBox cbDrawGrid; + private JCheckBox cbDrawTileGrid; + private JCheckBox cbDrawCellGrid; private JCheckBox cbEnableSchedules; private JComboBox cbZoomLevel; private JCheckBox cbLayerAmbientRange; @@ -345,8 +348,11 @@ private void init() { cbDrawClosed.setToolTipText("Draw opened or closed states of doors"); cbDrawClosed.addActionListener(getListeners()); - cbDrawGrid = new JCheckBox(LABEL_DRAW_GRID); - cbDrawGrid.addActionListener(getListeners()); + cbDrawTileGrid = new JCheckBox(LABEL_DRAW_TILE_GRID); + cbDrawTileGrid.addActionListener(getListeners()); + + cbDrawCellGrid = new JCheckBox(LABEL_DRAW_CELL_GRID); + cbDrawCellGrid.addActionListener(getListeners()); cbDrawOverlays = new JCheckBox(LABEL_DRAW_OVERLAYS); cbDrawOverlays.setToolTipText("Shows overlay tilesets.
" @@ -376,7 +382,8 @@ private void init() { t.add(new DefaultMutableTreeNode(bpwDayTime)); t.add(new DefaultMutableTreeNode(cbEnableSchedules)); t.add(new DefaultMutableTreeNode(cbDrawClosed)); - t.add(new DefaultMutableTreeNode(cbDrawGrid)); + t.add(new DefaultMutableTreeNode(cbDrawTileGrid)); + t.add(new DefaultMutableTreeNode(cbDrawCellGrid)); t2 = new DefaultMutableTreeNode(cbDrawOverlays); t2.add(new DefaultMutableTreeNode(cbAnimateOverlays)); t.add(t2); @@ -757,9 +764,13 @@ private void initGuiSettings() { setDoorState(Settings.DrawClosed); } - // initializing grid - cbDrawGrid.setSelected(Settings.DrawGrid); - setTileGridEnabled(Settings.DrawGrid); + // initializing tile grid + cbDrawTileGrid.setSelected(Settings.DrawTileGrid); + setTileGridEnabled(Settings.DrawTileGrid); + + // initializing cell grid + cbDrawCellGrid.setSelected(Settings.DrawCellGrid); + setCellGridEnabled(Settings.DrawCellGrid); // initializing overlays cbDrawOverlays.setSelected(Settings.DrawOverlays); @@ -872,10 +883,13 @@ private void updateWindowTitle() { overlayState = "disabled"; } - String gridState = isTileGridEnabled() ? "enabled" : "disabled"; + String tileGridState = isTileGridEnabled() ? "enabled" : "disabled"; + + String cellGridState = isCellGridEnabled() ? "enabled" : "disabled"; - setTitle(String.format("%s (Time: %02d:00 (%s), Schedules: %s, Doors: %s, Overlays: %s, Grid: %s, Zoom: %d%%)", - windowTitle, getHour(), dayNight, scheduleState, doorState, overlayState, gridState, zoom)); + setTitle(String.format("%s (Time: %02d:00 (%s), Schedules: %s, Doors: %s, Overlays: %s, Tile Grid: %s, " + + "Cell Grid: %s, Zoom: %d%%)", + windowTitle, getHour(), dayNight, scheduleState, doorState, overlayState, tileGridState, cellGridState, zoom)); } /** Sets day time to a specific hour (0..23). */ @@ -1069,14 +1083,28 @@ private void setDoorStateLayers(boolean closed) { /** Returns whether tile grid on map has been enabled. */ private boolean isTileGridEnabled() { - return Settings.DrawGrid; + return Settings.DrawTileGrid; } /** Enable/disable tile grid on map. */ private void setTileGridEnabled(boolean enable) { - Settings.DrawGrid = enable; + Settings.DrawTileGrid = enable; + if (rcCanvas != null) { + rcCanvas.setTileGridEnabled(Settings.DrawTileGrid); + } + updateWindowTitle(); + } + + /** Returns whether cell grid on map has been enabled. */ + private boolean isCellGridEnabled() { + return Settings.DrawCellGrid; + } + + /** Enable/disable cell grid on map. */ + private void setCellGridEnabled(boolean enable) { + Settings.DrawCellGrid = enable; if (rcCanvas != null) { - rcCanvas.setGridEnabled(Settings.DrawGrid); + rcCanvas.setCellGridEnabled(Settings.DrawCellGrid); } updateWindowTitle(); } @@ -2184,22 +2212,18 @@ private void exportMap() { try { final BufferedImage dstImage; if (isExportLayersEnabled()) { - double zoom = getZoomFactor(); - setZoomFactor(1.0, 1.0); - try { - dstImage = new BufferedImage(rcCanvas.getWidth(), rcCanvas.getHeight(), BufferedImage.TYPE_INT_RGB); - Graphics2D g1 = dstImage.createGraphics(); - rcCanvas.paint(g1); - g1.dispose(); - } finally { - setZoomFactor(zoom, Settings.ZoomFactor); - } + dstImage = new BufferedImage(rcCanvas.getWidth(), rcCanvas.getHeight(), BufferedImage.TYPE_INT_RGB); + final Graphics2D g1 = dstImage.createGraphics(); + rcCanvas.paint(g1); + g1.dispose(); } else { - VolatileImage srcImage = (VolatileImage) rcCanvas.getImage(); - dstImage = ColorConvert.createCompatibleImage(srcImage.getWidth(), srcImage.getHeight(), + final double zoom = getZoomFactor(); + final VolatileImage srcImage = (VolatileImage) rcCanvas.getImage(); + dstImage = ColorConvert.createCompatibleImage(rcCanvas.getWidth(), rcCanvas.getHeight(), srcImage.getTransparency()); - Graphics2D g2 = dstImage.createGraphics(); - g2.drawImage(srcImage, 0, 0, null); + final Graphics2D g2 = dstImage.createGraphics(); + final AffineTransform xform = AffineTransform.getScaleInstance(zoom, zoom); + g2.drawImage(srcImage, xform, null); g2.dispose(); } bRet = ImageIO.write(dstImage, "png", os); @@ -2303,13 +2327,20 @@ public void actionPerformed(ActionEvent event) { } finally { WindowBlocker.blockWindow(AreaViewer.this, false); } - } else if (cb == cbDrawGrid) { + } else if (cb == cbDrawTileGrid) { WindowBlocker.blockWindow(AreaViewer.this, true); try { setTileGridEnabled(cb.isSelected()); } finally { WindowBlocker.blockWindow(AreaViewer.this, false); } + } else if (cb == cbDrawCellGrid) { + WindowBlocker.blockWindow(AreaViewer.this, true); + try { + setCellGridEnabled(cb.isSelected()); + } finally { + WindowBlocker.blockWindow(AreaViewer.this, false); + } } else if (cb == cbDrawOverlays) { WindowBlocker.blockWindow(AreaViewer.this, true); try { diff --git a/src/org/infinity/resource/are/viewer/BackgroundAnimationProvider.java b/src/org/infinity/resource/are/viewer/BackgroundAnimationProvider.java index 8e6db0838..4e2e1ff85 100644 --- a/src/org/infinity/resource/are/viewer/BackgroundAnimationProvider.java +++ b/src/org/infinity/resource/are/viewer/BackgroundAnimationProvider.java @@ -5,7 +5,6 @@ package org.infinity.resource.are.viewer; import java.awt.AlphaComposite; -import java.awt.Color; import java.awt.Graphics2D; import java.awt.Point; import java.awt.Rectangle; @@ -21,8 +20,6 @@ * Implements functionality for properly displaying background animations. */ public class BackgroundAnimationProvider extends AbstractAnimationProvider { - private static final Color TRANSPARENT_COLOR = new Color(0, true); - // upscaled luma weights for use with faster right-shift (by 16) private static final int LUMA_R = 19595; private static final int LUMA_G = 38470; @@ -372,8 +369,8 @@ protected synchronized void updateGraphics() { // clearing old content Graphics2D g = image.createGraphics(); try { - g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC)); - g.setColor(TRANSPARENT_COLOR); + g.setComposite(AlphaComposite.Src); + g.setColor(ColorConvert.TRANSPARENT_COLOR); g.fillRect(0, 0, image.getWidth(), image.getHeight()); // rendering frame @@ -429,8 +426,8 @@ protected synchronized void updateGraphics() { } else { Graphics2D g = image.createGraphics(); try { - g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC)); - g.setColor(TRANSPARENT_COLOR); + g.setComposite(AlphaComposite.Src); + g.setColor(ColorConvert.TRANSPARENT_COLOR); g.fillRect(0, 0, image.getWidth(), image.getHeight()); } finally { g.dispose(); diff --git a/src/org/infinity/resource/are/viewer/Settings.java b/src/org/infinity/resource/are/viewer/Settings.java index 1aedb4f6b..1a1686108 100644 --- a/src/org/infinity/resource/are/viewer/Settings.java +++ b/src/org/infinity/resource/are/viewer/Settings.java @@ -63,7 +63,9 @@ public class Settings { // Current visibility state of overlays public static boolean DrawOverlays = getDefaultDrawOverlays(); // Current visibility state of the tile grid - public static boolean DrawGrid = getDefaultDrawGrid(); + public static boolean DrawTileGrid = getDefaultDrawTileGrid(); + // Current visibility state of the cell grid + public static boolean DrawCellGrid = getDefaultDrawCellGrid(); // Current visibility state of ambient range items public static boolean ShowAmbientRanges = getDefaultAmbientRanges(); // Current visibility state of container target locations @@ -132,7 +134,8 @@ public class Settings { private static final String PREFS_MOUSEWHEELZOOM = "MouseWheelZoom"; private static final String PREFS_DRAWCLOSED = "DrawClosed"; private static final String PREFS_DRAWOVERLAYS = "DrawOverlays"; - private static final String PREFS_DRAWGRID = "DrawGrid"; + private static final String PREFS_DRAWTILEGRID = "DrawGrid"; + private static final String PREFS_DRAWCELLGRID = "DrawCellGrid"; private static final String PREFS_SIDEBARCONTROLS = "SidebarControls"; private static final String PREFS_SHOWACTORFRAME = "ShowActorFrame"; private static final String PREFS_SHOWACTORSELECTION = "ShowActorSelectionCircle"; @@ -222,7 +225,8 @@ public static void loadSettings(boolean force) { EnableSchedules = prefs.getBoolean(PREFS_ENABLESCHEDULES, getDefaultEnableSchedules()); DrawClosed = prefs.getBoolean(PREFS_DRAWCLOSED, getDefaultDrawClosed()); DrawOverlays = prefs.getBoolean(PREFS_DRAWOVERLAYS, getDefaultDrawOverlays()); - DrawGrid = prefs.getBoolean(PREFS_DRAWGRID, getDefaultDrawGrid()); + DrawTileGrid = prefs.getBoolean(PREFS_DRAWTILEGRID, getDefaultDrawTileGrid()); + DrawCellGrid = prefs.getBoolean(PREFS_DRAWCELLGRID, getDefaultDrawCellGrid()); ShowAmbientRanges = prefs.getBoolean(PREFS_SHOWAMBIENT, getDefaultAmbientRanges()); ShowContainerTargets = prefs.getBoolean(PREFS_SHOWCONTAINERTARGETS, getDefaultShowContainerTargets()); ShowDoorTargets = prefs.getBoolean(PREFS_SHOWDOORTARGETS, getDefaultShowDoorTargets()); @@ -286,7 +290,8 @@ public static void storeSettings(boolean force) { prefs.putBoolean(PREFS_ENABLESCHEDULES, EnableSchedules); prefs.putBoolean(PREFS_DRAWCLOSED, DrawClosed); prefs.putBoolean(PREFS_DRAWOVERLAYS, DrawOverlays); - prefs.putBoolean(PREFS_DRAWGRID, DrawGrid); + prefs.putBoolean(PREFS_DRAWTILEGRID, DrawTileGrid); + prefs.putBoolean(PREFS_DRAWCELLGRID, DrawCellGrid); prefs.putBoolean(PREFS_SHOWAMBIENT, ShowAmbientRanges); prefs.putBoolean(PREFS_SHOWCONTAINERTARGETS, ShowContainerTargets); prefs.putBoolean(PREFS_SHOWDOORTARGETS, ShowDoorTargets); @@ -381,7 +386,11 @@ public static boolean getDefaultDrawOverlays() { return true; } - public static boolean getDefaultDrawGrid() { + public static boolean getDefaultDrawTileGrid() { + return false; + } + + public static boolean getDefaultDrawCellGrid() { return false; } diff --git a/src/org/infinity/resource/are/viewer/TilesetRenderer.java b/src/org/infinity/resource/are/viewer/TilesetRenderer.java index b1c147375..001a19e79 100644 --- a/src/org/infinity/resource/are/viewer/TilesetRenderer.java +++ b/src/org/infinity/resource/are/viewer/TilesetRenderer.java @@ -4,6 +4,7 @@ package org.infinity.resource.are.viewer; +import java.awt.AlphaComposite; import java.awt.Color; import java.awt.Dimension; import java.awt.Graphics; @@ -86,7 +87,8 @@ public enum RenderMode { private boolean hasChangedOverlays; private boolean hasChangedDoorState; private boolean isClosed = false; // opened/closed state of door tiles - private boolean showGrid = false; // indicates whether to draw a grid on the tiles + private boolean showTileGrid = false; // indicates whether to draw a grid on the tiles + private boolean showCellGrid = false; // indicates whether to draw a grid on the cells private boolean forcedInterpolation = false; // indicates whether to use a pre-defined interpolation type or set one // based on zoom factor private double zoomFactor = 1.0; // zoom factor for drawing the map @@ -346,13 +348,25 @@ public void setLighting(int lighting) { } } - public boolean isGridEnabled() { - return showGrid; + public boolean isTileGridEnabled() { + return showTileGrid; } - public void setGridEnabled(boolean enable) { - if (enable != showGrid) { - showGrid = enable; + public void setTileGridEnabled(boolean enable) { + if (enable != showTileGrid) { + showTileGrid = enable; + hasChangedAppearance = true; + updateDisplay(); + } + } + + public boolean isCellGridEnabled() { + return showCellGrid; + } + + public void setCellGridEnabled(boolean enable) { + if (enable != showCellGrid) { + showCellGrid = enable; hasChangedAppearance = true; updateDisplay(); } @@ -537,20 +551,11 @@ protected void updateSize() { @Override protected void paintCanvas(Graphics g) { super.paintCanvas(g); - if (showGrid) { - double tileWidth = 64.0 * zoomFactor; - double tileHeight = 64.0 * zoomFactor; - double mapWidth = getMapWidth(true); - double mapHeight = getMapHeight(true); - g.setColor(Color.GRAY); - for (double curY = 0.0; curY < mapHeight; curY += tileHeight) { - for (double curX = 0.0; curX < mapWidth; curX += tileWidth) { - g.drawLine((int) Math.ceil(curX), (int) Math.ceil(curY + tileHeight), (int) Math.ceil(curX + tileWidth), - (int) Math.ceil(curY + tileHeight)); - g.drawLine((int) Math.ceil(curX + tileWidth), (int) Math.ceil(curY), (int) Math.ceil(curX + tileWidth), - (int) Math.ceil(curY + tileHeight)); - } - } + if (showCellGrid) { + drawGrid(g, 16.0, 12.0, Color.DARK_GRAY); + } + if (showTileGrid) { + drawGrid(g, 64.0, 64.0, Color.GRAY); } } @@ -621,8 +626,9 @@ private void release(boolean forceUpdate) { if (forceUpdate) { Graphics2D g = (Graphics2D) img.getGraphics(); try { - g.setBackground(new Color(0, true)); - g.clearRect(0, 0, img.getWidth(null), img.getHeight(null)); + g.setComposite(AlphaComposite.Src); + g.setColor(ColorConvert.TRANSPARENT_COLOR); + g.fillRect(0, 0, img.getWidth(null), img.getHeight(null)); } finally { g.dispose(); } @@ -752,6 +758,31 @@ private boolean hasOverlay(int ovlIdx) { return false; } + // Draws a grid on the map with the specified parameters + private void drawGrid(Graphics g, double gridWidth, double gridHeight, Color color) { + if (g == null) { + System.err.println("TilesetRenderer.drawGrid: Graphics argument is null"); + return; + } + if (color == null) { + System.err.println("TilesetRenderer.drawGrid: Color argument is null"); + return; + } + final double gridWidthZoomed = gridWidth * zoomFactor; + final double gridHeightZoomed = gridHeight * zoomFactor; + final double mapWidth = getMapWidth(true); + final double mapHeight = getMapHeight(true); + g.setColor(color); + for (double curY = 0.0; curY < mapHeight; curY += gridHeightZoomed) { + for (double curX = 0.0; curX < mapWidth; curX += gridWidthZoomed) { + g.drawLine((int) Math.ceil(curX), (int) Math.ceil(curY + gridHeightZoomed), (int) Math.ceil(curX + gridWidthZoomed), + (int) Math.ceil(curY + gridHeightZoomed)); + g.drawLine((int) Math.ceil(curX + gridWidthZoomed), (int) Math.ceil(curY), (int) Math.ceil(curX + gridWidthZoomed), + (int) Math.ceil(curY + gridHeightZoomed)); + } + } + } + // draws all tiles of the map private void drawAllTiles() { final Tileset ts = listTilesets.get(0); diff --git a/src/org/infinity/resource/bcs/BcsTrigger.java b/src/org/infinity/resource/bcs/BcsTrigger.java index 0a0c0a940..79a78ab02 100644 --- a/src/org/infinity/resource/bcs/BcsTrigger.java +++ b/src/org/infinity/resource/bcs/BcsTrigger.java @@ -6,6 +6,8 @@ import java.awt.Point; import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; import org.infinity.resource.Profile; import org.infinity.util.StringBufferStream; @@ -168,74 +170,88 @@ public Signatures.Function getMatchingFunction() { if (functions.length == 1) { retVal = functions[0]; } else { - // weighting parameter types (strings are most important) - int weightI = 1; - int weightO = 2; - int weightP = 4; - int weightS = 8; - int bestScore = Integer.MAX_VALUE; // lower is better - Signatures.Function fallback = null; - for (Signatures.Function f : functions) { - int pi = 0, ps = 0, po = 0, pp = 0; + // Map of Signatures.Function.Parameter.TYPE_XXX > Number of non-default arguments for that parameter type + final HashMap paramMap = new HashMap<>(); + + int bestScoreAvg = Integer.MAX_VALUE; // lower is better; zero indicates a perfect match + int bestScoreVal = Integer.MAX_VALUE; // lower is better; zero indicates a perfect match + int bestNumParams = Integer.MAX_VALUE; // cosmetic: lower value indicates a shorter function signature + for (final Signatures.Function f : functions) { + paramMap.clear(); for (int i = 0, cnt = f.getNumParameters(); i < cnt; i++) { - Signatures.Function.Parameter param = f.getParameter(i); - switch (param.getType()) { - case Signatures.Function.Parameter.TYPE_INTEGER: - pi++; - break; - case Signatures.Function.Parameter.TYPE_STRING: - ps++; - break; - case Signatures.Function.Parameter.TYPE_OBJECT: - po++; - break; - case Signatures.Function.Parameter.TYPE_POINT: - pp++; - break; - } + final Signatures.Function.Parameter param = f.getParameter(i); + final char ptype = param.getType(); + final int pcount = paramMap.getOrDefault(ptype, 0); + paramMap.put(ptype, pcount + 1); } - // prefer function signature with string arguments for fallback solution - if (fallback == null && ps > 0) { - fallback = f; - } - - // evaluating remaining arguments + int scoreInt = 0; for (int i = 0; i < 3; i++) { - if (getNumericParam(i) != 0) { - pi--; + // checking int params + if ((getNumericParam(i) != 0)) { + scoreInt++; } } + int scoreStr = 0; for (int i = 0; i < 4; i++) { + // checking string params try { - if (!getStringParam(f, i).isEmpty()) { - ps--; + final Signatures.Function f2 = paramMap.containsKey(Signatures.Function.Parameter.TYPE_STRING) ? f : null; + if (!getStringParam(f2, i).isEmpty()) { + scoreStr++; } } catch (IllegalArgumentException e) { + // no more strings available break; } } - if (!getObjectParam(0).isEmpty()) { - po--; - } + // checking object param + int scoreObj = !getObjectParam(0).isEmpty() ? 1 : 0; - if (getPointParam(0).x != 0 || getPointParam(0).y != 0) { - pp--; + // checking point param + int scorePt = (getPointParam(0).x != 0 || getPointParam(0).y != 0) ? 1 : 0; + + for (final Map.Entry entry : paramMap.entrySet()) { + final char ptype = entry.getKey(); + final int pcount = entry.getValue(); + switch (ptype) { + case Signatures.Function.Parameter.TYPE_INTEGER: + scoreInt -= pcount; + break; + case Signatures.Function.Parameter.TYPE_STRING: + scoreStr -= pcount; + break; + case Signatures.Function.Parameter.TYPE_OBJECT: + scoreObj -= pcount; + break; + case Signatures.Function.Parameter.TYPE_POINT: + scorePt -= pcount; + break; + } } - // finding match - int score = Math.abs(pi * weightI + ps * weightS + po * weightO + pp * weightP); - if (score < bestScore) { - bestScore = score; + // How to determine a match: + // - no match stored yet OR + // - individual score is less OR <-- individual score <= 0: perfect match is found + // - individual score is equal AND + // - avg. score is less OR + // - avg. score is equal AND + // - parameter count is less <-- helps finding the most compact function signature if several are eligible + int numParams = f.getNumParameters(); + int scoreVal = Math.max(scoreInt, Math.max(scoreStr, Math.max(scoreObj, scorePt))); + int scoreAvg = Math.max(0, scoreInt + scoreStr + scoreObj + scorePt); + if (retVal == null || + scoreVal < bestScoreVal || + (scoreVal == bestScoreVal && (scoreAvg < bestScoreAvg || + (scoreAvg == bestScoreAvg && numParams < bestNumParams)))) { + bestNumParams = numParams; + bestScoreVal = scoreVal; + bestScoreAvg = scoreAvg; retVal = f; } } - - if (retVal == null) { - retVal = (fallback != null) ? fallback : functions[0]; - } } } diff --git a/src/org/infinity/resource/bcs/Decompiler.java b/src/org/infinity/resource/bcs/Decompiler.java index 614cffb7e..7fe4b4419 100644 --- a/src/org/infinity/resource/bcs/Decompiler.java +++ b/src/org/infinity/resource/bcs/Decompiler.java @@ -8,6 +8,7 @@ import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.HashSet; +import java.util.Iterator; import java.util.List; import java.util.Locale; import java.util.Set; @@ -36,6 +37,10 @@ public final class Decompiler { "itemflag", "jourtype", "magespec", "splcast", "state", "wmpflag") .collect(Collectors.toSet())); + static { + updateBitwiseIds(); + } + private final Set strrefsUsed = new HashSet<>(); private final Set resourcesUsed = new HashSet<>(); private final SortedMap idsErrors = new TreeMap<>(); @@ -1133,4 +1138,30 @@ private String getNormalizedString(String string) { return retVal; } + + /** Searches for IDS resources with only binary entries and adds them to the global list. */ + private static void updateBitwiseIds() { + final List idsList = ResourceFactory.getResources("ids"); + for (final ResourceEntry idsEntry : idsList) { + final String idsName = idsEntry.getResourceRef().toLowerCase(Locale.ROOT); + if (BITWISE_IDS.contains(idsName)) { + continue; + } + + final IdsMap idsMap = IdsMapCache.get(idsEntry.getResourceName()); + if (idsMap == null) { + continue; + } + + boolean binary = (idsMap.size() > 0); + for (final Iterator iter = idsMap.getKeys().iterator(); iter.hasNext() && binary; ) { + final long key = iter.next(); + binary = (key == 0 || Long.bitCount(key) == 1); + } + + if (binary) { + BITWISE_IDS.add(idsName); + } + } + } } diff --git a/src/org/infinity/resource/bcs/ScriptInfo.java b/src/org/infinity/resource/bcs/ScriptInfo.java index 83ec70c68..832a4c214 100644 --- a/src/org/infinity/resource/bcs/ScriptInfo.java +++ b/src/org/infinity/resource/bcs/ScriptInfo.java @@ -54,8 +54,6 @@ public class ScriptInfo { si.functionConcatMap.put(30, 0x0001); // SetGlobal si.functionConcatMap.put(109, 0x0001); // IncrementGlobal si.functionConcatMap.put(115, 0x0001); // SetGlobalTimer - si.functionConcatMap.put(141, 0x0001); // GivePartyGoldGlobal - si.functionConcatMap.put(165, 0x0001); // AddexperiencePartyGlobal si.functionParamCommentMap.put(151, 1); // DisplayString si.functionParamCommentMap.put(197, 1); // MoveGlobal @@ -171,8 +169,6 @@ public class ScriptInfo { si.functionConcatMap.put(30, 0x0001); // SetGlobal si.functionConcatMap.put(109, 0x0001); // IncrementGlobal si.functionConcatMap.put(115, 0x0001); // SetGlobalTimer - si.functionConcatMap.put(141, 0x0001); // GivePartyGoldGlobal - si.functionConcatMap.put(165, 0x0001); // AddexperiencePartyGlobal si.functionConcatMap.put(243, 0x0011); // IncrementGlobalOnce si.functionConcatMap.put(0x40A5, 0x0101); // BitGlobal si.functionConcatMap.put(247, 0x0101); // BitGlobal @@ -234,8 +230,6 @@ public class ScriptInfo { si.functionConcatMap.put(30, 0x0001); // SetGlobal si.functionConcatMap.put(109, 0x0001); // IncrementGlobal si.functionConcatMap.put(115, 0x0001); // SetGlobalTimer - si.functionConcatMap.put(141, 0x0001); // GivePartyGoldGlobal - si.functionConcatMap.put(165, 0x0001); // AddexperiencePartyGlobal si.functionConcatMap.put(308, 0x0001); // SetGlobalTimerOnce si.functionConcatMap.put(243, 0x0011); // IncrementGlobalOnce si.functionConcatMap.put(0x40A5, 0x0101); // BitGlobal @@ -293,8 +287,6 @@ public class ScriptInfo { si.functionConcatMap.put(30, 0x0001); // SetGlobal si.functionConcatMap.put(109, 0x0001); // IncrementGlobal si.functionConcatMap.put(115, 0x0001); // SetGlobalTimer - si.functionConcatMap.put(141, 0x0001); // GivePartyGoldGlobal - si.functionConcatMap.put(165, 0x0001); // AddexperiencePartyGlobal si.functionConcatMap.put(227, 0x0001); // GlobalBAND si.functionConcatMap.put(228, 0x0001); // GlobalBOR si.functionConcatMap.put(229, 0x0001); // GlobalSHR diff --git a/src/org/infinity/resource/chu/Viewer.java b/src/org/infinity/resource/chu/Viewer.java index 7fcfe3819..4d333670f 100644 --- a/src/org/infinity/resource/chu/Viewer.java +++ b/src/org/infinity/resource/chu/Viewer.java @@ -57,6 +57,7 @@ import org.infinity.resource.graphics.BamDecoder.BamControl; import org.infinity.resource.graphics.BamDecoder.FrameEntry; import org.infinity.resource.graphics.BamV1Decoder.BamV1Control; +import org.infinity.resource.graphics.ColorConvert; import org.infinity.resource.graphics.MosDecoder; import org.infinity.resource.graphics.MosV1Decoder; import org.infinity.util.StringTable; @@ -1328,7 +1329,7 @@ public void updateImage() { try { // 1. clearing image g.setComposite(AlphaComposite.Src); - g.setColor(new Color(0, true)); + g.setColor(ColorConvert.TRANSPARENT_COLOR); g.fillRect(0, 0, image.getWidth(), image.getHeight()); // 2. drawing control @@ -1445,7 +1446,7 @@ public void updateImage() { // 1. clearing image Composite comp = g.getComposite(); g.setComposite(AlphaComposite.Src); - g.setColor(new Color(0, true)); + g.setColor(ColorConvert.TRANSPARENT_COLOR); g.fillRect(0, 0, image.getWidth(), image.getHeight()); g.setComposite(comp); @@ -1558,7 +1559,7 @@ public void updateImage() { // 1. clearing image Composite comp = g.getComposite(); g.setComposite(AlphaComposite.Src); - g.setColor(new Color(0, true)); + g.setColor(ColorConvert.TRANSPARENT_COLOR); g.fillRect(0, 0, image.getWidth(), image.getHeight()); g.setComposite(comp); @@ -1672,7 +1673,7 @@ public void updateImage() { // 1. clearing image Composite comp = g.getComposite(); g.setComposite(AlphaComposite.Src); - g.setColor(new Color(0, true)); + g.setColor(ColorConvert.TRANSPARENT_COLOR); g.fillRect(0, 0, image.getWidth(), image.getHeight()); g.setComposite(comp); @@ -1787,7 +1788,7 @@ public void updateImage() { // 1. clearing image Composite comp = g.getComposite(); g.setComposite(AlphaComposite.Src); - g.setColor(new Color(0, true)); + g.setColor(ColorConvert.TRANSPARENT_COLOR); g.fillRect(0, 0, image.getWidth(), image.getHeight()); g.setComposite(comp); @@ -1833,7 +1834,7 @@ public void updateImage() { // 1. clearing image Composite comp = g.getComposite(); g.setComposite(AlphaComposite.Src); - g.setColor(new Color(0, true)); + g.setColor(ColorConvert.TRANSPARENT_COLOR); g.fillRect(0, 0, image.getWidth(), image.getHeight()); g.setComposite(comp); @@ -1949,7 +1950,7 @@ public void updateImage() { // 1. clearing image Composite comp = g.getComposite(); g.setComposite(AlphaComposite.Src); - g.setColor(new Color(0, true)); + g.setColor(ColorConvert.TRANSPARENT_COLOR); g.fillRect(0, 0, image.getWidth(), image.getHeight()); g.setComposite(comp); diff --git a/src/org/infinity/resource/cre/ViewerAnimation.java b/src/org/infinity/resource/cre/ViewerAnimation.java index 0acaa11e9..b08e438f8 100644 --- a/src/org/infinity/resource/cre/ViewerAnimation.java +++ b/src/org/infinity/resource/cre/ViewerAnimation.java @@ -6,7 +6,6 @@ import java.awt.AlphaComposite; import java.awt.BorderLayout; -import java.awt.Color; import java.awt.Container; import java.awt.Dimension; import java.awt.Graphics2D; @@ -47,13 +46,13 @@ import org.infinity.resource.cre.decoder.SpriteDecoder.SpriteBamControl; import org.infinity.resource.cre.decoder.util.Sequence; import org.infinity.resource.cre.decoder.util.SpriteUtils; +import org.infinity.resource.graphics.ColorConvert; /** * A basic creature animation viewer. */ public class ViewerAnimation extends JComponent implements ActionListener { - private static final Color COLOR_TRANSPARENT = new Color(0, true); - private static final int ANIM_DELAY = 1000 / 15; // 15 fps in milliseconds + private static final int ANIM_DELAY = 1000 / 15; // 15 fps in milliseconds private static boolean zoom = false; private static boolean showSelectionCircle = false; @@ -169,7 +168,7 @@ public void updateCanvas() { Graphics2D g = tmpImage.createGraphics(); try { g.setComposite(AlphaComposite.Src); - g.setColor(COLOR_TRANSPARENT); + g.setColor(ColorConvert.TRANSPARENT_COLOR); g.fillRect(0, 0, tmpImage.getWidth(), tmpImage.getHeight()); } finally { g.dispose(); @@ -181,7 +180,7 @@ public void updateCanvas() { try { // clearing old content g.setComposite(AlphaComposite.Src); - g.setColor(COLOR_TRANSPARENT); + g.setColor(ColorConvert.TRANSPARENT_COLOR); g.fillRect(0, 0, rcDisplay.getImage().getWidth(null), rcDisplay.getImage().getHeight(null)); // drawing markers diff --git a/src/org/infinity/resource/cre/browser/RenderPanel.java b/src/org/infinity/resource/cre/browser/RenderPanel.java index fc68cdcda..2c99ddb14 100644 --- a/src/org/infinity/resource/cre/browser/RenderPanel.java +++ b/src/org/infinity/resource/cre/browser/RenderPanel.java @@ -36,7 +36,6 @@ * This panel handles drawing background and creature animations. */ public class RenderPanel extends JPanel { - private static final Color COLOR_TRANSPARENT = new Color(0, true); private static final float POS_REL_X = 0.5f; private static final float POS_REL_Y = 2.0f / 3.0f; @@ -219,7 +218,7 @@ public Couple setFrame(SpriteBamControl ctrl, Image frame, Rec Graphics2D g = (Graphics2D) frame.getGraphics(); try { g.setComposite(AlphaComposite.Src); - g.setColor(background != null ? background : COLOR_TRANSPARENT); + g.setColor(background != null ? background : ColorConvert.TRANSPARENT_COLOR); g.fillRect(0, 0, frame.getWidth(null), frame.getHeight(null)); } finally { g.dispose(); diff --git a/src/org/infinity/resource/cre/decoder/SpriteDecoder.java b/src/org/infinity/resource/cre/decoder/SpriteDecoder.java index a277925a5..0a93a0bc0 100644 --- a/src/org/infinity/resource/cre/decoder/SpriteDecoder.java +++ b/src/org/infinity/resource/cre/decoder/SpriteDecoder.java @@ -1222,7 +1222,7 @@ protected int createFrame(FrameInfo[] sourceFrames, BeforeSourceFrame beforeSrcF Graphics2D g = image.createGraphics(); try { g.setComposite(AlphaComposite.SrcOver); - g.setColor(new Color(0, true)); + g.setColor(ColorConvert.TRANSPARENT_COLOR); g.fillRect(0, 0, image.getWidth(), image.getHeight()); // drawing source frames to target image diff --git a/src/org/infinity/resource/cre/decoder/tables/InfinityTables.java b/src/org/infinity/resource/cre/decoder/tables/InfinityTables.java index 5ffa65c93..5adcfa3c4 100644 --- a/src/org/infinity/resource/cre/decoder/tables/InfinityTables.java +++ b/src/org/infinity/resource/cre/decoder/tables/InfinityTables.java @@ -53,8 +53,12 @@ public static List createIniMaps(int animationId) { return retVal; } - IdsMap table = new IdsMap(entry); - retVal.addAll(processTable(table, animationId)); + try { + IdsMap table = new IdsMap(entry); + retVal.addAll(processTable(table, animationId)); + } catch (Exception e) { + e.printStackTrace(); + } return retVal; } diff --git a/src/org/infinity/resource/dlg/TreeViewer.java b/src/org/infinity/resource/dlg/TreeViewer.java index 31ddb1dab..84cdc8787 100644 --- a/src/org/infinity/resource/dlg/TreeViewer.java +++ b/src/org/infinity/resource/dlg/TreeViewer.java @@ -45,12 +45,14 @@ import javax.swing.tree.TreeSelectionModel; import org.infinity.NearInfinity; +import org.infinity.gui.InfinityTextArea; import org.infinity.gui.LinkButton; import org.infinity.gui.ScriptTextArea; import org.infinity.gui.StructViewer; import org.infinity.gui.ViewFrame; import org.infinity.gui.ViewerUtil; import org.infinity.gui.WindowBlocker; +import org.infinity.gui.menu.BrowserMenuBar; import org.infinity.icon.Icons; import org.infinity.resource.StructEntry; import org.infinity.util.StringTable; @@ -783,7 +785,8 @@ private JTextArea createReadOnlyTextArea() { /** Helper method for creating a ScriptTextArea component. */ private ScriptTextArea createScriptTextArea(boolean readOnly) { - ScriptTextArea ta = new ScriptTextArea(); + ScriptTextArea ta = new ScriptTextArea(InfinityTextArea.Language.DLG, + BrowserMenuBar.isInstantiated() && BrowserMenuBar.getInstance().getOptions().getDlgSyntaxHighlightingEnabled()); if (readOnly) { ta.setBackground(COLOR_BACKGROUND); ta.setHighlightCurrentLine(false); diff --git a/src/org/infinity/resource/gam/Viewer.java b/src/org/infinity/resource/gam/Viewer.java index 44d4e99c6..b7554f194 100644 --- a/src/org/infinity/resource/gam/Viewer.java +++ b/src/org/infinity/resource/gam/Viewer.java @@ -9,16 +9,27 @@ import java.awt.GridBagLayout; import java.awt.GridLayout; import java.awt.Insets; +import java.awt.Point; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.util.Objects; import javax.swing.BorderFactory; import javax.swing.DefaultListCellRenderer; import javax.swing.JList; +import javax.swing.JMenuItem; import javax.swing.JPanel; +import javax.swing.JPopupMenu; import org.infinity.datatype.Flag; import org.infinity.datatype.ResourceRef; +import org.infinity.gui.ViewFrame; import org.infinity.gui.ViewerUtil; import org.infinity.gui.ViewerUtil.ListValueRenderer; +import org.infinity.gui.ViewerUtil.StructListPanel; +import org.infinity.icon.Icons; import org.infinity.resource.AbstractStruct; import org.infinity.resource.AbstractVariable; import org.infinity.resource.StructEntry; @@ -95,10 +106,12 @@ private static JPanel makeMiscPanel(GamResource gam) { } Viewer(GamResource gam) { - final JPanel stats1Panel = ViewerUtil.makeListPanel("Non-player characters", gam, NonPartyNPC.class, NPC_ENTRY); - final JPanel stats2Panel = ViewerUtil.makeListPanel("Player characters", gam, PartyNPC.class, NPC_ENTRY); + final StructListPanel stats1Panel = ViewerUtil.makeListPanel("Non-player characters", gam, NonPartyNPC.class, NPC_ENTRY); + new NpcContextMenu(stats1Panel); + final StructListPanel stats2Panel = ViewerUtil.makeListPanel("Player characters", gam, PartyNPC.class, NPC_ENTRY); + new NpcContextMenu(stats2Panel); - JPanel var1Panel = ViewerUtil.makeListPanel("Variables", gam, Variable.class, AbstractVariable.VAR_NAME, + StructListPanel var1Panel = ViewerUtil.makeListPanel("Variables", gam, Variable.class, AbstractVariable.VAR_NAME, new VariableListRenderer()); setLayout(new GridLayout(2, 3, 3, 3)); @@ -137,4 +150,119 @@ public String getListValue(Object value) { return ""; } } + + /** + * Handles a context menu associated with a {@code PartyNPC} list panel. + */ + private static class NpcContextMenu extends MouseAdapter implements ActionListener { + private final JPopupMenu popup = new JPopupMenu(); + private final JMenuItem miOpenChr = new JMenuItem("View/Edit CHR", Icons.ICON_ZOOM_16.getIcon()); + private final JMenuItem miOpenCre = new JMenuItem("View/Edit CRE", Icons.ICON_ZOOM_16.getIcon()); + + private final StructListPanel panel; + private final JList npcList; + + public NpcContextMenu(StructListPanel npcPanel) { + this.panel = Objects.requireNonNull(npcPanel); + this.npcList = Objects.requireNonNull(this.panel.getList()); + init(); + } + +// /** Returns the associated {@code StructListPanel} instance. */ +// public StructListPanel getPanel() { +// return panel; +// } + +// /** Returns the {@code JPopupMenu} associated with the list panel. */ +// public JPopupMenu getPopupMenu() { +// return popup; +// } + + @Override + public void mousePressed(MouseEvent e) { + showPopup(e, true); + } + + @Override + public void mouseReleased(MouseEvent e) { + showPopup(e, true); + } + + @Override + public void actionPerformed(ActionEvent e) { + if (miOpenChr.equals(e.getSource())) { + final PartyNPC npc = getSelectedNpc(null, false); + if (npc != null) { + new ViewFrame(panel.getTopLevelAncestor(), npc); + } + } else if (miOpenCre.equals(e.getSource())) { + final PartyNPC npc = getSelectedNpc(null, false); + if (npc != null) { + final StructEntry se = npc.getAttribute(PartyNPC.GAM_NPC_CRE_RESOURCE); + if (se instanceof CreResource) { + new ViewFrame(panel.getTopLevelAncestor(), (CreResource) se); + } + } + } + } + + /** + * Triggers the context menu addressed by the specified {@code MouseEvent}. + * + * @param e {@code MouseEvent} to handle. + * @param autoSelect Indicates whether the list item at the current mouse coordinate should be selected. + */ + private void showPopup(MouseEvent e, boolean autoSelect) { + if (e.isPopupTrigger()) { + final PartyNPC npc = getSelectedNpc(new Point(e.getX(), e.getY()), autoSelect); + if (npc != null) { + miOpenCre.setEnabled(hasCreData(npc)); + popup.show(e.getComponent(), e.getX(), e.getY()); + } + } + } + + /** + * Returns whether the specified {@code PartyNPC} structure contains a valid CRE resource substructure. + */ + private boolean hasCreData(PartyNPC npc) { + return npc != null && npc.getAttribute(PartyNPC.GAM_NPC_CRE_RESOURCE) instanceof CreResource; + } + + /** + * Returns the NPC list element at the specified coordinate. + * + * @param p A location relative to the {@code npcList} component. Can be {@code null}. + * @param autoSelect Indicates whether the list item at the specified location should be selected. + * @return Returns the {@code PartyNPC} list item closest to the specified location. Returns the selected list item + * if location is {@code null}. + */ + private PartyNPC getSelectedNpc(Point p, boolean autoSelect) { + if (p != null && autoSelect) { + int index = npcList.locationToIndex(p); + if (index >= 0) { + npcList.setSelectedIndex(index); + } + } + + int index = (p != null && !autoSelect) ? npcList.locationToIndex(p) : npcList.getSelectedIndex(); + if (index >= 0) { + final Object listItem = npcList.getModel().getElementAt(index); + if (listItem instanceof PartyNPC) { + return (PartyNPC) listItem; + } + } + return null; + } + + private void init() { + miOpenChr.addActionListener(this); + miOpenCre.addActionListener(this); + + popup.add(miOpenChr); + popup.add(miOpenCre); + + npcList.addMouseListener(this); + } + } } diff --git a/src/org/infinity/resource/graphics/BamResource.java b/src/org/infinity/resource/graphics/BamResource.java index e355f669c..7c1be00d9 100644 --- a/src/org/infinity/resource/graphics/BamResource.java +++ b/src/org/infinity/resource/graphics/BamResource.java @@ -6,7 +6,6 @@ import java.awt.AlphaComposite; import java.awt.BorderLayout; -import java.awt.Color; import java.awt.Component; import java.awt.Dimension; import java.awt.Graphics2D; @@ -107,8 +106,6 @@ */ public class BamResource implements Resource, Closeable, Writeable, Referenceable, ActionListener, PropertyChangeListener, ChangeListener, IDataChangedListener { - private static final Color TransparentColor = new Color(0, true); - private static final int ANIM_DELAY = 1000 / 15; // 15 fps in milliseconds private static final ButtonPanel.Control CTRL_NEXT_CYCLE = ButtonPanel.Control.CUSTOM_1; @@ -539,8 +536,8 @@ public JComponent makeViewer(ViewableContainer container) { } List list = new ArrayList<>(); - if (miExport != null) { - list.add(miExport); + if (miExportFramesPNG != null) { + list.add(miExportFramesPNG); } if (miExportBAM != null) { list.add(miExportBAM); @@ -548,15 +545,15 @@ public JComponent makeViewer(ViewableContainer container) { if (miExportBAMC != null) { list.add(miExportBAMC); } - if (miExportFramesPNG != null) { - list.add(miExportFramesPNG); + if (miExport != null) { + list.add(miExport); } JMenuItem[] mi = new JMenuItem[list.size()]; for (int i = 0; i < mi.length; i++) { mi[i] = list.get(i); } ButtonPopupMenu bpmExport = (ButtonPopupMenu) ButtonPanel.createControl(ButtonPanel.Control.EXPORT_MENU); - bpmExport.setMenuItems(mi); + bpmExport.setMenuItems(mi, false); JButton bProperties = new JButton("Properties...", Icons.ICON_EDIT_16.getIcon()); bProperties.addActionListener(this); @@ -683,8 +680,8 @@ public void updateCanvas() { BufferedImage image = (BufferedImage) rcDisplay.getImage(); Graphics2D g = image.createGraphics(); try { - g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC)); - g.setColor(TransparentColor); + g.setComposite(AlphaComposite.Src); + g.setColor(ColorConvert.TRANSPARENT_COLOR); g.fillRect(0, 0, image.getWidth(), image.getHeight()); } finally { g.dispose(); diff --git a/src/org/infinity/resource/graphics/ColorConvert.java b/src/org/infinity/resource/graphics/ColorConvert.java index b4c63ff56..14d3673a6 100644 --- a/src/org/infinity/resource/graphics/ColorConvert.java +++ b/src/org/infinity/resource/graphics/ColorConvert.java @@ -4,6 +4,7 @@ package org.infinity.resource.graphics; +import java.awt.Color; import java.awt.Dimension; import java.awt.Graphics2D; import java.awt.GraphicsConfiguration; @@ -92,6 +93,12 @@ public class ColorConvert { lab2.getValue1(), lab2.getValue2(), alpha2); }; + /** Bitmask for the alpha channel of an ARGB color value. */ + public static final int ALPHA_MASK = 0xff000000; + + /** A {@link Color} definition with alpha=0 (fully transparent). */ + public static final Color TRANSPARENT_COLOR = new Color(0, true); + // Cache for ARGB key -> CIELAB color space values private static final HashMap> ARGB_LAB_CACHE = new HashMap<>(); diff --git a/src/org/infinity/resource/graphics/DxtEncoder.java b/src/org/infinity/resource/graphics/DxtEncoder.java index e26618ddc..cec5f7f97 100644 --- a/src/org/infinity/resource/graphics/DxtEncoder.java +++ b/src/org/infinity/resource/graphics/DxtEncoder.java @@ -29,6 +29,13 @@ package org.infinity.resource.graphics; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Future; + +import org.infinity.util.Threading; +import org.infinity.util.tuples.Couple; + /** * Encodes pixel data into the DXT1/DXT3/DXT5 format. */ @@ -41,7 +48,7 @@ public static enum DxtType { } /** - * Encodes an image into a series of DXTn code blocks. + * Encodes an image into a series of DXTn code blocks. Multithreading is used to speed up the encoding process. * * @param pixels The pixel data as array of integers in ARGB format. * @param width The width of the image (must be a multiple of 4). @@ -75,17 +82,33 @@ static public byte[] encodeImage(final int[] pixels, final int width, final int } /** - * Encodes an image into a series of DXTn code blocks. + * Encodes an image into a series of DXTn code blocks. Multithreading is used to speed up the encoding process. * - * @param pixels The pixel data as array of integers in ARGB format. - * @param width The width of the image (must be a multiple of 4). - * @param height The height of the image (must be a multiple of 4). - * @param output The storage space for the compressed data. - * @param dxtType The compression type to use. + * @param pixels The pixel data as array of integers in ARGB format. + * @param width The width of the image (must be a multiple of 4). + * @param height The height of the image (must be a multiple of 4). + * @param output The storage space for the compressed data. + * @param dxtType The compression type to use. * @throws Exception */ static public void encodeImage(final int[] pixels, final int width, final int height, final byte[] output, final DxtType dxtType) throws Exception { + encodeImage(pixels, width, height, output, dxtType, true); + } + + /** + * Encodes an image into a series of DXTn code blocks. + * + * @param pixels The pixel data as array of integers in ARGB format. + * @param width The width of the image (must be a multiple of 4). + * @param height The height of the image (must be a multiple of 4). + * @param output The storage space for the compressed data. + * @param dxtType The compression type to use. + * @param multithreaded Specify {@code true} to use multiple threads of execution to speed up encoding. + * @throws Exception + */ + static public void encodeImage(final int[] pixels, final int width, final int height, final byte[] output, + final DxtType dxtType, boolean multithreaded) throws Exception { // consistency check if (dxtType == null) throw new Exception("No DXT type specified"); @@ -99,23 +122,64 @@ static public void encodeImage(final int[] pixels, final int width, final int he throw new Exception(String.format("Insufficient space in output array. Needed: %d bytes, available: %d bytes", calcImageSize(width, height, dxtType), (output == null) ? 0 : output.length)); - int outputOfs = 0; // points to the end of encoded data - final int bw = width / 4; - final int bh = height / 4; - final int[] inBlock = new int[16]; - final byte[] outBlock = new byte[calcBlockSize(dxtType)]; - for (int y = 0; y < bh; y++) { - for (int x = 0; x < bw; x++) { - // create 4x4 block of pixels for DXTn compression - int ofs = (y * 4) * width + (x * 4); - for (int i = 0; i < 4; i++, ofs += width) { - System.arraycopy(pixels, ofs, inBlock, i * 4, 4); + // preparing thread pool + final Threading.Priority priority = multithreaded ? Threading.Priority.HIGHEST : Threading.Priority.LOWEST; + try (final Threading threadPool = new Threading(priority)) { + final int bw = width / 4; + final int bh = height / 4; + final List, Exception>>> futureList = new ArrayList<>(bh); + for (int y = 0; y < bh; y++) { + final List rowBlocks = new ArrayList<>(bw); + + for (int x = 0; x < bw; x++) { + // create 4x4 block of pixels for DXTn compression + final int[] inBlock = new int[16]; + int ofs = (y * 4) * width + (x * 4); + for (int i = 0; i < 4; i++, ofs += width) { + System.arraycopy(pixels, ofs, inBlock, i * 4, 4); + } + rowBlocks.add(inBlock); } - // compress pixel block - encodeBlock(inBlock, outBlock, dxtType); - System.arraycopy(outBlock, 0, output, outputOfs, outBlock.length); - outputOfs += outBlock.length; + // encoding one row per task + final Future, Exception>> future = threadPool.submit(() -> { + try { + final List outList = new ArrayList<>(rowBlocks.size()); + for (final int[] inBlock : rowBlocks) { + final byte[] outBlock = new byte[calcBlockSize(dxtType)]; + // compress pixel block + encodeBlock(inBlock, outBlock, dxtType); + outList.add(outBlock); + } + return new Couple<>(outList, null); + } catch (Exception e) { + return new Couple<>(null, e); + } + }); + futureList.add(future); + } + + threadPool.shutdown(); + + // assembling encoded blocks to output image + int outputOfs = 0; // points to the end of encoded data + for (final Future, Exception>> future : futureList) { + final Couple, Exception> item = future.get(); + final List list = item.getValue0(); + if (list != null) { + for (final byte[] outBlock : list) { + System.arraycopy(outBlock, 0, output, outputOfs, outBlock.length); + outputOfs += outBlock.length; + } + } else { + final Exception e = item.getValue1(); + if (e != null) { + throw e; + } else { + final int idx = outputOfs / calcBlockSize(dxtType); + throw new Exception("Could not encode block at index " + idx); + } + } } } } diff --git a/src/org/infinity/resource/graphics/GifSequenceReader.java b/src/org/infinity/resource/graphics/GifSequenceReader.java index f31eb2e15..6f29be0bd 100644 --- a/src/org/infinity/resource/graphics/GifSequenceReader.java +++ b/src/org/infinity/resource/graphics/GifSequenceReader.java @@ -160,23 +160,23 @@ private Frame initFrame(int index, IIOImage ioimg) throws Exception { g = bimg.createGraphics(); switch (dm) { case DO_NOT_DISPOSE: // Copy previous frame to current frame - g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC)); + g.setComposite(AlphaComposite.Src); g.drawImage(framePrev.getRenderedImage(), null, 0, 0); break; case PREVIOUS: // Restore content from two frame ago if (index > 1) { Frame fpp = frames.get(index - 2); - g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC)); + g.setComposite(AlphaComposite.Src); g.drawImage(fpp.getRenderedImage(), null, 0, 0); } break; case BACKGROUND: // Clear with background color - Color col = new Color(0, true); + Color col = ColorConvert.TRANSPARENT_COLOR; if (framePrev.getTransparentIndex() >= 0) { IndexColorModel cm = (IndexColorModel) framePrev.getImage().getColorModel(); col = new Color(cm.getRGB(framePrev.getTransparentIndex()), true); } - g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC)); + g.setComposite(AlphaComposite.Src); g.setColor(col); g.fillRect(0, 0, bimg.getWidth(), bimg.getHeight()); break; @@ -190,7 +190,7 @@ private Frame initFrame(int index, IIOImage ioimg) throws Exception { if (g != null) { // Rendering current frame - g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER)); + g.setComposite(AlphaComposite.SrcOver); g.drawImage(gifImg, null, frame.getRect().x, frame.getRect().y); g.dispose(); g = null; diff --git a/src/org/infinity/resource/graphics/MosResource.java b/src/org/infinity/resource/graphics/MosResource.java index ad23b0c2a..017c6c2ea 100644 --- a/src/org/infinity/resource/graphics/MosResource.java +++ b/src/org/infinity/resource/graphics/MosResource.java @@ -302,8 +302,8 @@ public JComponent makeViewer(ViewableContainer container) { } } List list = new ArrayList<>(); - if (miExport != null) { - list.add(miExport); + if (miExportPNG != null) { + list.add(miExportPNG); } if (miExportMOSV1 != null) { list.add(miExportMOSV1); @@ -311,15 +311,15 @@ public JComponent makeViewer(ViewableContainer container) { if (miExportMOSC != null) { list.add(miExportMOSC); } - if (miExportPNG != null) { - list.add(miExportPNG); + if (miExport != null) { + list.add(miExport); } JMenuItem[] mi = new JMenuItem[list.size()]; for (int i = 0; i < mi.length; i++) { mi[i] = list.get(i); } ButtonPopupMenu bpmExport = (ButtonPopupMenu) buttonPanel.addControl(ButtonPanel.Control.EXPORT_MENU); - bpmExport.setMenuItems(mi); + bpmExport.setMenuItems(mi, false); JButton bProperties = new JButton("Properties...", Icons.ICON_EDIT_16.getIcon()); bProperties.addActionListener(this); diff --git a/src/org/infinity/resource/graphics/MosV1Decoder.java b/src/org/infinity/resource/graphics/MosV1Decoder.java index 8676f7648..4c1de7962 100644 --- a/src/org/infinity/resource/graphics/MosV1Decoder.java +++ b/src/org/infinity/resource/graphics/MosV1Decoder.java @@ -455,7 +455,7 @@ private boolean renderMos(Image image) { if (image != null && blockCount > 0) { Graphics2D g = (Graphics2D) image.getGraphics(); try { - g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC)); // overwriting target regardless of transparency + g.setComposite(AlphaComposite.Src); // overwriting target regardless of transparency int imgWidth = image.getWidth(null); int imgHeight = image.getHeight(null); for (int i = 0; i < blockCount; i++) { diff --git a/src/org/infinity/resource/graphics/MosV2Decoder.java b/src/org/infinity/resource/graphics/MosV2Decoder.java index 1441ecba3..b572a299e 100644 --- a/src/org/infinity/resource/graphics/MosV2Decoder.java +++ b/src/org/infinity/resource/graphics/MosV2Decoder.java @@ -362,7 +362,7 @@ private boolean renderBlock(MosBlock block, Image canvas, int left, int top) { BufferedImage imgBlock = decoder.decode(pvrzRect.x, pvrzRect.y, pvrzRect.width, pvrzRect.height); Graphics2D g = (Graphics2D) canvas.getGraphics(); try { - g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC)); + g.setComposite(AlphaComposite.Src); g.drawImage(imgBlock, left, top, left + w, top + h, 0, 0, w, h, null); } finally { g.dispose(); diff --git a/src/org/infinity/resource/graphics/PltResource.java b/src/org/infinity/resource/graphics/PltResource.java index f4e5f6f0a..b4ed02478 100644 --- a/src/org/infinity/resource/graphics/PltResource.java +++ b/src/org/infinity/resource/graphics/PltResource.java @@ -211,7 +211,7 @@ public JComponent makeViewer(ViewableContainer container) { miExport.addActionListener(this); miExportPNG = new JMenuItem("as PNG"); miExportPNG.addActionListener(this); - bpmExport.setMenuItems(new JMenuItem[] { miExport, miExportPNG }, false); + bpmExport.setMenuItems(new JMenuItem[] { miExportPNG, miExport }, false); ((JButton) buttonPanel.addControl(ButtonPanel.Control.SAVE)).addActionListener(this); buttonPanel.getControlByType(ButtonPanel.Control.SAVE).setEnabled(false); diff --git a/src/org/infinity/resource/graphics/PseudoBamDecoder.java b/src/org/infinity/resource/graphics/PseudoBamDecoder.java index 7bf693489..191824438 100644 --- a/src/org/infinity/resource/graphics/PseudoBamDecoder.java +++ b/src/org/infinity/resource/graphics/PseudoBamDecoder.java @@ -5,7 +5,6 @@ package org.infinity.resource.graphics; import java.awt.AlphaComposite; -import java.awt.Color; import java.awt.Dimension; import java.awt.Graphics2D; import java.awt.Image; @@ -70,8 +69,6 @@ public class PseudoBamDecoder extends BamDecoder { /** A value specifying the number of data blocks (BAM v2 specific) [Integer] */ public static final String OPTION_INT_BLOCKCOUNT = "BlockCount"; - private static final Color TRANSPARENT_COLOR = new Color(0, true); - private final PseudoBamFrameEntry defaultFrameInfo = new PseudoBamFrameEntry(null, 0, 0); private final HashMap mapOptions = new HashMap<>(); @@ -1314,7 +1311,7 @@ private boolean createPvrzPages(Path path, DxtEncoder.DxtType dxtType, List init(int tileIndex, BufferedImage tileImage, TisDecoder decoder, + TileInfo tileInfo) throws Exception { + final int tileSize = 64; + final boolean isPrimary = (tileInfo.getPrimaryTileFrame(tileIndex) >= 0); + + final BufferedImage tileImage2 = ColorConvert.createCompatibleImage(tileSize, tileSize, true); + final Couple retVal = new Couple<>(null, null); + if (isPrimary) { + decoder.getTile(tileInfo.tileSecondary, tileImage2); + retVal.setValue0(tileImage); + retVal.setValue1(tileImage2); + } else { + decoder.getTile(tileInfo.tilePrimary, tileImage2); + retVal.setValue0(tileImage2); + retVal.setValue1(tileImage); + } + + return retVal; + } + } + + /** Status definitions used by conversion routines. */ + public enum Status { + /** Conversion completed successfully. */ + SUCCESS, + /** Conversion was cancelled by the user. */ + CANCELLED, + /** Conversion failed with an error. */ + ERROR, + /** Conversion does not meet the requirements. */ + UNSUPPORTED + } + + /** List of available tileset overlay conversion modes. */ + public enum OverlayConversion { + /** Do not convert tileset overlays. */ + NONE("No conversion", overlayUpdaterDefault, overlayConverterDefault, true), + /** Convert BG1 tileset overlays using BGEE-style. */ + BG1_TO_BGEE("BG1 -> BGEE", overlayUpdaterBG1toBGEE, overlayConverterBG1toBGEE, false), + /** Convert BG1 tileset overlays using BG2EE-style. */ + BG1_TO_BG2EE("BG1 -> BG2EE", overlayUpdaterBG1toBG2EE, overlayConverterBG1toBG2EE, false), + /** Convert BG2 tileset overlays using BGEE-style. */ + BG2_TO_BGEE("BG2 -> BGEE", overlayUpdaterBG2toBGEE, overlayConverterBG2toBGEE, false), + /** Convert BG2 tileset overlays using BG2EE-style. */ + BG2_TO_BG2EE("BG2 -> BG2EE", overlayUpdaterDefault, overlayConverterBG2toBG2EE, true), + + /** Convert BGEE tileset overlays using BG1-style. */ + BGEE_TO_BG1("BGEE -> BG1", overlayUpdaterBGEEtoBG1, overlayConverterBGEEtoBG1, false), + /** Convert BGEE tileset overlays using BG2-style. */ + BGEE_TO_BG2("BGEE -> BG2", overlayUpdaterBGEEtoBG2, overlayConverterBGEEtoBG2, false), + /** Convert BG2EE tileset overlays using BG1-style. */ + BG2EE_TO_BG1("BG2EE -> BG1", overlayUpdaterBG2EEtoBG1, overlayConverterBG2EEtoBG1, false), + /** Convert BG2EE tileset overlays using BG2-style. */ + BG2EE_TO_BG2("BG2EE -> BG2", overlayUpdaterDefault, overlayConverterBG2EEtoBG2, true), + ; + + private final String label; + private final boolean implemented; + private final OverlayMapUpdater updater; + private final OverlayTileConverter converter; + + private OverlayConversion(String label, OverlayMapUpdater updater, OverlayTileConverter converter, + boolean implemented) { + this.label = label; + this.implemented = implemented; + this.updater = Objects.requireNonNull(updater); + this.converter = Objects.requireNonNull(converter); + } + + /** Returns whether the conversion has been implemented. */ + public boolean isImplemented() { + return implemented; + } + + /** Returns a reference to the {@link TilsetUpdater} instance. */ + private OverlayMapUpdater getUpdater() { + return updater; + } + + /** Returns a reference to the {@link TilsetConverter} instance. */ + private OverlayTileConverter getConverter() { + return converter; + } + + @Override + public String toString() { + return label; + } + } + + /** + * Composes an image from a list of tiles and exports it as PNG file. + * + * @param tiles List of tiles, ordered left-to-right, to-to-bottom. + * @param tileCols Number of tiles per row. + * @param pngFile {@link Path} of the PNG output file. + * @return {@link Status} that indicates the completion state of the conversion. + */ + public static Status exportPNG(List tiles, int tileCols, Path pngFile) { + return exportPNG(tiles, tileCols, pngFile, false, null); + } + + /** + * Composes an image from a list of tiles and exports it as PNG file. + * + * @param tiles List of tiles, ordered left-to-right, to-to-bottom. + * @param tileCols Number of tiles per row. + * @param pngFile {@link Path} of the PNG output file. + * @param showProgress Indicates whether a progress dialog is shown during the conversion process. + * @param parent Parent component of the progress dialog. + * @return {@link Status} that indicates the completion state of the conversion. + */ + public static Status exportPNG(List tiles, int tileCols, Path pngFile, boolean showProgress, Component parent) { + Status retVal = Status.ERROR; + if (pngFile == null || tiles == null || tiles.isEmpty() || tileCols < 1) { + return retVal; + } + + int tileRows = (tiles.size() + tileCols - 1) / tileCols; + + if (showProgress && parent == null) { + parent = NearInfinity.getInstance(); + } + + if (tileCols > 0 && tileRows > 0) { + BufferedImage image = null; + ProgressMonitor progress = null; + if (showProgress) { + progress = new ProgressMonitor(parent, "Exporting TIS to PNG...", "", 0, 2); + progress.setMillisToDecideToPopup(0); + progress.setMillisToPopup(0); + progress.setProgress(0); + } + + image = ColorConvert.createCompatibleImage(tileCols * 64, tileRows * 64, Transparency.BITMASK); + Graphics2D g = image.createGraphics(); + for (int idx = 0; idx < tiles.size(); idx++) { + if (tiles.get(idx) != null) { + int tx = idx % tileCols; + int ty = idx / tileCols; + g.drawImage(tiles.get(idx), tx * 64, ty * 64, null); + } + } + g.dispose(); + + if (showProgress) { + progress.setProgress(1); + } + + try (OutputStream os = StreamUtils.getOutputStream(pngFile, true)) { + if (ImageIO.write(image, "png", os)) { + retVal = Status.SUCCESS; + } + } catch (IOException e) { + retVal = Status.ERROR; + e.printStackTrace(); + } + + if (showProgress && progress.isCanceled()) { + retVal = Status.CANCELLED; + } + + if (showProgress) { + progress.close(); + progress = null; + } + } + + if (retVal != Status.SUCCESS && FileEx.create(pngFile).isFile()) { + try { + Files.delete(pngFile); + } catch (IOException e) { + e.printStackTrace(); + } + } + + return retVal; + } + + /** + * Checks whether the given TIS filename can be used to generate PVRZ filenames from. + * + * @param fileName TIS filename. + * @return {@code true} if the TIS filename is valid for PVRZ file generation, {@code false} otherwise. + */ + public static boolean isTisFileNameValid(Path fileName) { + if (fileName != null) { + String name = fileName.getFileName().toString(); + int extOfs = name.lastIndexOf('.'); + if (extOfs >= 0) { + name = name.substring(0, extOfs); + } + return Pattern.matches(".{2,7}", name); + } + return false; + } + + /** + * Attempts to fix the given filename to make it compatible with the naming scheme of PVRZ-based TIS files. + * + * @param fileName TIS filename. + * @return TIS filename that resolves to a valid PVRZ filename scheme. + */ + public static Path makeTisFileNameValid(Path fileName) { + if (fileName != null && !isTisFileNameValid(fileName)) { + Path path = fileName.getParent(); + String name = fileName.getFileName().toString(); + String ext = ""; + int extOfs = name.lastIndexOf('.'); + if (extOfs >= 0) { + ext = name.substring(extOfs); + name = name.substring(0, extOfs); + } + + boolean isNight = (Character.toUpperCase(name.charAt(name.length() - 1)) == 'N'); + if (name.length() > 7) { + int numDelete = name.length() - 7; + int ofsDelete = name.length() - numDelete - (isNight ? 1 : 0); + name = name.substring(ofsDelete, numDelete); + return path.resolve(name); + } else if (name.length() < 2) { + String fmt, newName = null; + int maxNum; + switch (name.length()) { + case 0: + fmt = name + "%s02d"; + maxNum = 99; + break; + default: + fmt = name + "%s01d"; + maxNum = 9; + break; + } + for (int i = 0; i < maxNum; i++) { + String s = String.format(fmt, i) + (isNight ? "N" : "") + ext; + if (!ResourceFactory.resourceExists(s)) { + newName = s; + break; + } + } + if (newName != null) { + return path.resolve(newName); + } + } + } + return fileName; + } + + /** + * Attempts to find and return a WED resource that references the given TIS resource. + * + * @param tisEntry {@link ResourceEntry} instance of the TIS resource. + * @param deepSearch Specify {@code false} to only check for a WED file of same resref as the given TIS + * resource.Specify {@code true} to check all available WED files for a match. + * @return {@link ResourceEntry} to the WED resource that references {@code tisEntry}. Returns {@code null} if not + * found. + */ + public static ResourceEntry findWed(ResourceEntry tisEntry, boolean deepSearch) { + if (tisEntry == null) { + return null; + } + + final Predicate wedCheck = wedEntry -> { + if (wedEntry != null) { + try { + final int[] resInfo = tisEntry.getResourceInfo(); + int numTiles = (resInfo != null && resInfo.length > 1) ? resInfo[0] : 0; + if (numTiles > 0) { + final ByteBuffer buf = wedEntry.getResourceBuffer().order(ByteOrder.LITTLE_ENDIAN); + final String sig = StreamUtils.readString(buf, 0, 8); + if ("WED V1.3".equals(sig) && buf.getInt(0x08) > 0) { + final int ofsOvl = buf.getInt(0x10); + final int w = buf.getShort(ofsOvl); + final int h = buf.getShort(ofsOvl + 2); + final String tisRef = StreamUtils.readString(buf, ofsOvl + 4, 8); + return (numTiles >= w*h && tisRef != null && tisRef.equalsIgnoreCase(tisEntry.getResourceRef())); + } + } + } catch (IOException e) { + // do nothing + } catch (Exception e) { + System.err.println(e.getClass().getName() + ": " + e.getMessage()); + } + } + return false; + }; + + // trying WED of same name first + ResourceEntry wedEntry = ResourceFactory.getResourceEntry(tisEntry.getResourceRef() + ".WED"); + boolean retVal = wedCheck.test(wedEntry); + + // searching WED + if (!retVal && deepSearch) { + final List wedEntries = ResourceFactory.getResources("WED"); + wedEntry = wedEntries + .parallelStream() + .filter(wedCheck) + .findAny() + .orElse(null); + } + + return wedEntry; + } + + /** + * Attempts to find and load the WED resource associated with the specified TIS resource. + * + * @param tisEntry The TIS resource entry. + * @param deepSearch Specify {@code false} to only check for a WED file of same resref as the given TIS + * resource.Specify {@code true} to check all available WED files for a match. + * @return {@code WedResource} instance if successful, {@code null} otherwise. + */ + public static WedResource loadWedForTis(ResourceEntry tisEntry, boolean deepSearch) { + WedResource wed = null; + + final ResourceEntry wedEntry = findWed(tisEntry, deepSearch); + if (wedEntry != null) { + try { + wed = new WedResource(wedEntry); + } catch (Exception e) { + // ignored + } + } + + return wed; + } + + /** + * Attempts to calculate the TIS width from an associated WED file. + * + * @param entry {@code ResourceEntry} for a TIS or WED resource. + * @param deepSearch Specify {@code false} to only check for a WED file of same resref as the given TIS + * resource.Specify {@code true} to check all available WED files for a match. + * @param tileCount An optional tile count that will be used to "guess" the correct number of tiles per row if no + * associated WED resource has been found. + * @return The number of tiles per row for the specified TIS resource. + */ + public static int calcTilesetWidth(ResourceEntry entry, boolean deepSearch, int tileCount) { + // Try to fetch the correct width from an associated WED if available + if (entry != null) { + final ResourceEntry wedEntry = (entry.getExtension().equalsIgnoreCase("wed")) ? entry : findWed(entry, deepSearch); + if (wedEntry != null) { + try { + final ByteBuffer buf = wedEntry.getResourceBuffer().order(ByteOrder.LITTLE_ENDIAN); + final int ofsOvl = buf.getInt(0x10); + final int w = buf.getShort(ofsOvl); + if (w > 0) { + return w; + } + } catch (Exception e) { + // ignored + } + } + } + + // If WED is not available: approximate the most commonly used aspect ratio found in TIS files + // Disadvantage: does not take extra tiles into account + return (tileCount < 9) ? tileCount : (int) (Math.sqrt(tileCount) * 1.18); + } + + /** + * Returns the first available PVRZ filename page index in the path of the specified {@code tisFile}. + * + * @param tisFile {@link Path} of the output TIS file. + * @return PVRZ base index for the specified TIS file. Returns {@code 0} if page index could not be determined. + */ + public static int calcPvrzBaseIndex(Path tisFile) { + int retVal = 0; + if (tisFile != null) { + for (int i = 0; i < 100; i++) { + final Path pvrzFile = generatePvrzFileName(tisFile, i); + if (!Files.exists(pvrzFile)) { + retVal = i; + break; + } + } + } + return retVal; + } + + /** + * Attempts to determine the movement type of the tileset from the associated WED resource. + * + * @param entry {@code ResourceEntry} for a TIS or WED resource. + * @param deepSearch Specify {@code false} to only check for a WED file of same resref as the given TIS + * resource.Specify {@code true} to check all available WED files for a match. + * @return The movement type value for EE games and if a WED resource was found. Otherwise, {@code 0} is returned. + */ + public static int getTisMovementType(ResourceEntry entry, boolean deepSearch) { + int retVal = 0; + + if (!Profile.isEnhancedEdition()) { + return retVal; + } + + if (entry != null) { + final ResourceEntry wedEntry = (entry.getExtension().equalsIgnoreCase("wed")) ? entry : findWed(entry, deepSearch); + if (wedEntry != null) { + try { + final ByteBuffer buf = wedEntry.getResourceBuffer().order(ByteOrder.LITTLE_ENDIAN); + final int ofsOvl = buf.getInt(0x10); + retVal = buf.getShort(ofsOvl + 0x0e); + } catch (Exception e) { + // ignored + } + } + } + + return retVal; + } + + /** + * Converts the given tileset into the palette-based variant. + * + * @param config {@link Config} instance with global conversion parameters. + * @param showProgress Indicates whether a progress dialog should be displayed. + * @param parent Parent component for the the progress dialog. + * @return A status value that indicates success of the conversion operation. + */ + public static Status convertToPaletteTis(Config config, boolean showProgress, Component parent) { + Status retVal = Status.ERROR; + if (config == null) { + return retVal; + } + + if (showProgress && parent == null) { + parent = NearInfinity.getInstance(); + } + + final String fmtNote = "Converting tile %d / %d"; + final int progressMax = config.getDecoder().getTileCount(); + int progressIndex = 0; + final ProgressMonitor progress; + if (showProgress) { + progress = new ProgressMonitor(parent, "Converting TIS...", String.format(fmtNote, progressIndex, progressMax), + 0, progressMax); + progress.setMillisToDecideToPopup(250); + progress.setMillisToPopup(1000); + } else { + progress = null; + } + + final List tiles = config.getTileList(); + final TisV2Decoder decoder = (TisV2Decoder) config.getDecoder(); + final WedInfo wedInfo = config.getWedInfo(); + final OverlayConversion conversionMode = config.getOverlayConversion(); + + if (conversionMode.isImplemented()) { + conversionMode.getUpdater().update(wedInfo); + } + + try (BufferedOutputStream bos = new BufferedOutputStream(Files.newOutputStream(config.getTisFile()))) { + retVal = Status.SUCCESS; + + // writing TIS header + final byte[] header = new byte[24]; + System.arraycopy("TIS V1 ".getBytes(), 0, header, 0, 8); + // TODO: use different tile count source when implementing overlay conversion modes that change tileset layout + DynamicArray.putInt(header, 0x08, decoder.getTileCount()); + DynamicArray.putInt(header, 0x0c, 0x1400); + DynamicArray.putInt(header, 0x10, 0x18); + DynamicArray.putInt(header, 0x14, 0x40); + bos.write(header); + + // processing TIS data + final int[] palette = new int[255]; + final byte[] tilePalette = new byte[256 * 4]; + final byte[] tileData = new byte[Config.TILE_SIZE * Config.TILE_SIZE]; + BufferedImage tileImageOut = + ColorConvert.createCompatibleImage(Config.TILE_SIZE, Config.TILE_SIZE, Transparency.BITMASK); + final IntegerHashMap colorCache = new IntegerHashMap<>(1800); // caching RGB -> index + for (int tileIdx = 0, tileCount = decoder.getTileCount(); tileIdx < tileCount; tileIdx++) { + colorCache.clear(); + + if (showProgress) { + progressIndex++; + if ((progressIndex % 100) == 0) { + final int curProgressIndex = progressIndex; + SwingUtilities.invokeLater(() -> { + progress.setProgress(curProgressIndex); + progress.setNote(String.format(fmtNote, curProgressIndex, progressMax)); + }); + } + if (progress.isCanceled()) { + return Status.CANCELLED; + } + } + + final Graphics2D g = tileImageOut.createGraphics(); + try { + g.setComposite(AlphaComposite.Src); + g.drawImage(tiles.get(tileIdx), 0, 0, null); + } finally { + g.dispose(); + } + + // overlay conversion + if (conversionMode.isImplemented()) { + final Point tileLocation = wedInfo.getTileLocation(tileIdx); + if (tileLocation != null) { + final int priTileIdx = tileLocation.y * wedInfo.getWidth() + tileLocation.x; + final TileInfo tileInfo = wedInfo.getTile(priTileIdx); + tileImageOut = conversionMode.getConverter().convert(tileIdx, tileImageOut, decoder, tileInfo); + } + } + + int[] pixels = ((DataBufferInt) tileImageOut.getRaster().getDataBuffer()).getData(); + if (ColorConvert.medianCut(pixels, 255, palette, true)) { + // filling palette + // first palette entry denotes transparency + tilePalette[0] = tilePalette[2] = tilePalette[3] = 0; + tilePalette[1] = (byte) 255; + for (int i = 1; i < 256; i++) { + tilePalette[(i << 2) + 0] = (byte) (palette[i - 1] & 0xff); + tilePalette[(i << 2) + 1] = (byte) ((palette[i - 1] >>> 8) & 0xff); + tilePalette[(i << 2) + 2] = (byte) ((palette[i - 1] >>> 16) & 0xff); + tilePalette[(i << 2) + 3] = 0; + colorCache.put(palette[i - 1], (byte) (i - 1)); + } + + // filling pixel data + for (int i = 0; i < tileData.length; i++) { + if ((pixels[i] & 0xff000000) == 0) { + tileData[i] = 0; + } else { + final Byte palIndex = colorCache.get(pixels[i]); + if (palIndex != null) { + tileData[i] = (byte) (palIndex + 1); + } else { + byte color = (byte) ColorConvert.getNearestColor(pixels[i], palette, 0.0, null); + tileData[i] = (byte) (color + 1); + colorCache.put(pixels[i], color); + } + } + } + } else { + retVal = Status.ERROR; + break; + } + bos.write(tilePalette); + bos.write(tileData); + } + tileImageOut.flush(); + } catch (Exception e) { + retVal = Status.ERROR; + e.printStackTrace(); + } finally { + if (showProgress) { + SwingUtilities.invokeLater(() -> progress.close()); + } + if (retVal == Status.ERROR) { + // deleting incomplete tis file + try { + Files.delete(config.getTisFile()); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + + return retVal; + } + + /** + * Converts the given tileset into the pvrz-based variant. + * + * @param config {@link Config} instance with global conversion parameters. + * @param showProgress Indicates whether a progress dialog should be displayed. + * @param parent Parent component for the the progress dialog. + * @return A status value that indicates success of the conversion operation. + */ + public static Status convertToPvrzTis(Config config, boolean showProgress, Component parent) { + Status retVal = Status.ERROR; + if (config == null) { + return retVal; + } + + if (showProgress && parent == null) { + parent = NearInfinity.getInstance(); + } + + final ProgressMonitor progress; + if (showProgress) { + progress = new ProgressMonitor(parent, "Converting TIS...", "Preparing TIS", 0, 6); + progress.setMillisToDecideToPopup(0); + progress.setMillisToPopup(0); + SwingUtilities.invokeLater(() -> progress.setProgress(1)); + } else { + progress = null; + } + + try { + final TisV1Decoder decoder = (TisV1Decoder) config.getDecoder(); + final WedInfo wedInfo = config.getWedInfo(); + final int width = wedInfo.getWidth(); + final int height = wedInfo.getHeight(); + final boolean detectBlack = config.isDetectBlack(); + final int borderSize = config.getBorderSize(); + final OverlayConversion overlayConversion = config.getOverlayConversion(); + final int segmentSize = config.getSegmentSize(); + + // handling overlay conversion (layout changes) + if (wedInfo.hasWedResource() && overlayConversion.isImplemented()) { + overlayConversion.getUpdater().update(wedInfo); + } + + final List regions = new ArrayList<>(); + // marks indices of tiles that have already been processed + final BitSet markedTiles = new BitSet(decoder.getTileCount()); + + // processing primary tiles + final TileMap tmBase = new TileMap(wedInfo); + final int numTiles = decoder.getTileCount(); + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + final int idx = y * width + x; + if (idx < numTiles) { + final TileInfo ti = wedInfo.getTile(idx); + if (ti.tilePrimary >= 0 && !markedTiles.get(ti.tilePrimary)) { + if (!detectBlack || !isTileBlack(decoder, ti.tilePrimary)) { + tmBase.setTile(x, y, ti.tilePrimary, TileMapItem.FLAG_ALL); + markedTiles.set(ti.tilePrimary); + } + } + } + } + } + final List primaryTilesList = createTileRegions(config, tmBase); + regions.addAll(primaryTilesList); + + if (showProgress) { + SwingUtilities.invokeLater(() -> { + progress.setProgress(2); + progress.setNote("Processing tiles"); + }); + if (progress.isCanceled()) { + return Status.CANCELLED; + } + } + + // processing animated primary tiles + for (int i = 0, count = wedInfo.getTileCount(); i < count; i++) { + final TileInfo ti = wedInfo.getTile(i); + if (ti.tilesPrimary.length > 1) { + final int x = i % wedInfo.getWidth(); + final int y = i / wedInfo.getWidth(); + // frame 0 was already covered previously + for (int j = 1; j < ti.tilesPrimary.length; j++) { + if (!markedTiles.get(ti.tilesPrimary[j])) { + if (!detectBlack || !isTileBlack(decoder, ti.tilesPrimary[j])) { + final TileMap tm = new TileMap(wedInfo); + tm.setTile(x, y, ti.tilesPrimary[j], TileMapItem.FLAG_ALL); + markedTiles.set(ti.tilesPrimary[j]); + final List list = createTileRegions(config, tm); + regions.addAll(list); + } + } + } + } + } + + // processing door tiles + for (int i = 0, count = wedInfo.getDoorCount(); i < count; i++) { + if (wedInfo.getDoorTileCount(i) > 0) { + // adding door tiles + final TileMap tm = new TileMap(wedInfo); + final int[] indices = wedInfo.getDoorTileIndices(i); + for (final int idx : indices) { + if (idx < 0 || idx >= wedInfo.getWidth() * wedInfo.getHeight()) { + final int tileCount = wedInfo.getWidth() * wedInfo.getHeight(); + throw new IndexOutOfBoundsException(getErrorMessage(config, + "Door tile is out of bounds (index=" + idx + ", size=" + tileCount + ")")); + } + final int x = idx % wedInfo.getWidth(); + final int y = idx / wedInfo.getWidth(); + final TileInfo ti = wedInfo.getTile(idx); + if (ti.tileSecondary >= 0 && !markedTiles.get(ti.tileSecondary)) { + if (!detectBlack || !isTileBlack(decoder, ti.tileSecondary)) { + tm.setTile(x, y, ti.tileSecondary, TileMapItem.FLAG_ALL); + markedTiles.set(ti.tileSecondary); + } + } + } + // Reduce texture size hard limit to reduce the amount of wasted space on pvr textures + final List list = createTileRegions(new Config(config).setTextureSize(segmentSize), tm); + regions.addAll(list); + } + } + + // processing secondary tiles not covered by previous operations + final TileMap mapAll = new TileMap(wedInfo); + for (int i = 0, count = wedInfo.getTileCount(); i < count; i++) { + final TileInfo ti = wedInfo.getTile(i); + if (ti.tileSecondary >= 0 && !markedTiles.get(ti.tileSecondary)) { + if (!detectBlack || !isTileBlack(decoder, ti.tileSecondary)) { + final int x = i % wedInfo.getWidth(); + final int y = i / wedInfo.getWidth(); + mapAll.setTile(x, y, ti.tileSecondary, TileMapItem.FLAG_ALL); + markedTiles.set(ti.tileSecondary); + } + } + } + // splitting map into distinct chunks + List mapList = mapAll.split(); + for (final TileMap tm : mapList) { + if (!tm.isEmpty()) { + // Reduce texture size hard limit to reduce the amount of wasted space on pvr textures + final List list = createTileRegions(new Config(config).setTextureSize(segmentSize), tm); + regions.addAll(list); + } + } + + if (showProgress) { + SwingUtilities.invokeLater(() -> { + progress.setProgress(3); + progress.setNote("Generating tile mapping"); + }); + if (progress.isCanceled()) { + return Status.CANCELLED; + } + } + + // mapping tile regions to textures + final List mappedTileList = new ArrayList<>(numTiles); + final List pageList = new ArrayList<>(); + final BinPack2D.HeuristicRules binPackRule = BinPack2D.HeuristicRules.BOTTOM_LEFT_RULE; + for (final TileMap tileMap : regions) { + final Rectangle bounds = tileMap.getPixelBounds(borderSize); + Rectangle binRect = null; + int pageIndex = -1; + + // checking whether the map fits into existing bins + for (int i = 0, size = pageList.size(); i < size; i++) { + final BinPack2D bin = pageList.get(i); + binRect = bin.insert(bounds.width, bounds.height, binPackRule); + if (!binRect.isEmpty()) { + pageIndex = i; + break; + } + } + + // creating new bin? + if (pageIndex < 0) { + final BinPack2D bin = new BinPack2D(config.getTextureSize(), config.getTextureSize()); + pageList.add(bin); + pageIndex = pageList.size() - 1; + binRect = bin.insert(bounds.width, bounds.height, binPackRule); + } + + // basic error checks + if (pageIndex < 0 || binRect == null || binRect.isEmpty()) { + // should never happen + throw new Exception(getErrorMessage(config, "Could not fit tile region into texture page. Region: " + bounds)); + } + if (config.getPvrzBaseIndex() + pageIndex >= 100) { + throw new Exception(getErrorMessage(config, "Tileset requires too many texture pages")); + } + + tileMap.setPageRect(config.getPvrzBaseIndex() + pageIndex, binRect, borderSize); + + // assigning TIS mapping to tiles + final List locations = tileMap.getAllTilePositions(false); + for (final Point p : locations) { + final TileMapItem tmi = tileMap.getTile(p); + if (tmi.isAllFlag()) { + final ConvertToTis.TileEntry tileEntry = + new ConvertToTis.TileEntry(tmi.getIndex(), tmi.getPage(), tmi.getX(), tmi.getY()); + mappedTileList.add(tileEntry); + } + } + } + mappedTileList.sort(ConvertToTis.TileEntry.CompareByIndex); + + // generating pvrz files + final String fmtPvrzProgress = "Writing PVRZ (%d / %d)"; + for (int i = 0, size = pageList.size(); i < size; i++) { + final int pageIdx = i; + final int effectivePageIdx = config.getPvrzBaseIndex() + pageIdx; + + if (showProgress) { + SwingUtilities.invokeLater(() -> { + progress.setProgress(4); + progress.setNote(String.format(fmtPvrzProgress, pageIdx + 1, pageList.size())); + }); + if (progress.isCanceled()) { + return Status.CANCELLED; + } + } + + final List tileMaps = regions + .stream() + .filter(tm -> tm.getPage() == effectivePageIdx) + .collect(Collectors.toList()); + final Path pvrzPath = generatePvrzFileName(config.getTisFile(), effectivePageIdx); + if (pvrzPath == null) { + throw new Exception(getErrorMessage(config, "Could not determine pvrz file name")); + } + + final BinPack2D bin = pageList.get(pageIdx); + bin.shrinkBin(true); + + createPvrz(config, pvrzPath, tileMaps, bin.getBinWidth(), bin.getBinHeight()); + } + + // generating output TIS file + if (showProgress) { + SwingUtilities.invokeLater(() -> { + progress.setProgress(5); + progress.setNote("Writing TIS"); + }); + if (progress.isCanceled()) { + return Status.CANCELLED; + } + } + createPvrzTis(config, mappedTileList); + + retVal = Status.SUCCESS; + } catch (Exception e) { + retVal = Status.ERROR; + e.printStackTrace(); + } finally { + if (showProgress) { + SwingUtilities.invokeLater(() -> progress.close()); + } + } + + return retVal; + } + + /** + * Determines whether the tile at the specified index is completely black. + * + * @param decoder {@link TisDecoder} instance. + * @param tileIndex Tile index. + * @return {@code true} if the tile is completely black (ie. no colored or transparent pixels), {@code false} + * otherwise. + */ + private static boolean isTileBlack(TisDecoder decoder, int tileIndex) { + Objects.requireNonNull(decoder); + boolean retVal = true; + if (tileIndex < 0 || tileIndex >= decoder.getTileCount() || !(decoder instanceof TisV1Decoder)) { + return retVal; + } + + final TisV1Decoder decoder2 = (TisV1Decoder) decoder; + + // loading palette data + final int[] pal = new int[256]; + decoder2.getTilePalette(tileIndex, pal, true); + + // loading raw tile data + final byte[] tileData = new byte[Config.TILE_SIZE * Config.TILE_SIZE]; + decoder2.getRawTileData(tileIndex, tileData); + + // checking whether tile consists of solid color + final int transparentColor = 0x0000ff00; + int palIdx = -1; + for (int idx = 0; (idx < tileData.length) && retVal; idx++) { + if (palIdx == -1) { + palIdx = tileData[idx] & 0xff; + // exception: transparent palette entry doesn't count + retVal = (pal[palIdx] != transparentColor); + } + retVal &= (tileData[idx] == palIdx); + } + + if (retVal && palIdx >= 0) { + // evaluating solid tile color + final int threshold = 8; // color should be (near) black + int r = (pal[palIdx] >> 16) & 0xff; + int g = (pal[palIdx] >> 8) & 0xff; + int b = pal[palIdx] & 0xff; + retVal = (r < threshold) && (g < threshold) && (b < threshold); + } + + return retVal; + } + + /** + * Splits the given {@code tileMap} into regions that fit onto textures of up to {@link Config#getTextureSize()} + * pixels. Regions are returned as a list. + * + * @param config {@link Config} instance with global conversion parameters. + * @param tileMap {@link TileMap} structure with the tiles to split. + * @return List of {@link TileMap} objects that are small enough to fit onto a texture of {@code pageSize} pixels. + */ + private static List createTileRegions(Config config, TileMap tileMap) { + final List retVal = new ArrayList<>(); + if (config == null || tileMap == null || tileMap.isEmpty()) { + return retVal; + } + + final WedInfo wedInfo = config.getWedInfo(); + final int width = wedInfo.getWidth(); + final int height = wedInfo.getHeight(); + final int textureSize = config.getTextureSize(); + final int borderSize = config.getBorderSize(); + final Set locations = new HashSet<>(tileMap.getAllTilePositions(false)); + final Rectangle tileBounds = tileMap.getTileBounds(true); + + int x0 = 0; + int y0 = 0; + final Rectangle regionBounds = new Rectangle(tileBounds.x + x0, tileBounds.y + y0, 0, 0); + while (x0 < tileBounds.width && y0 < tileBounds.height) { + final TileMap tm = new TileMap(wedInfo); + + // calculating horizontal tile map size + int borderLeft = (tileBounds.x + x0 > 0) ? borderSize : 0; + int numTilesX = Math.min(textureSize / Config.TILE_SIZE, tileBounds.width - x0); + while (numTilesX * Config.TILE_SIZE + borderLeft > textureSize) { + numTilesX--; + } + int borderRight = (tileBounds.x + x0 + numTilesX < width) ? borderSize : 0; + while (numTilesX * Config.TILE_SIZE + borderLeft + borderRight > textureSize) { + numTilesX--; + } + + // calculating vertical tile map size + int borderTop = (tileBounds.y + y0 > 0) ? borderSize : 0; + int numTilesY = Math.min(textureSize / Config.TILE_SIZE, tileBounds.height - y0); + while (numTilesY * Config.TILE_SIZE + borderTop > textureSize) { + numTilesY--; + } + int borderBottom = (tileBounds.y + y0 + numTilesY < height) ? borderSize : 0; + while (numTilesY * Config.TILE_SIZE + borderTop + borderBottom > textureSize) { + numTilesY--; + } + + regionBounds.width = numTilesX; + regionBounds.height = numTilesY; + + final Point p2 = new Point(); + for (final Iterator iter = locations.iterator(); iter.hasNext(); ) { + final Point p = iter.next(); + if (regionBounds.contains(p)) { + final TileMapItem tileMapItem = tileMap.getTile(p); + if (tileMapItem.getIndex() < 0) { + continue; + } + + // adding main tile + final TileInfo tiPrimary = wedInfo.getTile(p); + int framePrimary = tiPrimary.getPrimaryTileFrame(tileMapItem.getIndex()); + boolean isSecondary = (tileMapItem.getIndex() == tiPrimary.tileSecondary); + tm.setTile(p, tileMapItem.getIndex(), TileMapItem.FLAG_ALL); + + // adding left border? + p2.x = p.x - 1; p2.y = p.y; + if (p2.x >= 0 && (p2.x == regionBounds.x - 1 || !locations.contains(p2))) { + final TileInfo ti = wedInfo.getTile(p2); + final int tileIdx = (isSecondary && ti.tileSecondary >= 0) ? ti.tileSecondary : ti.getPrimaryTileIndex(framePrimary); + if (tileIdx >= 0) { + final TileMapItem tmi = tm.getTile(p2); + if (tmi != null) { + tmi.setRightFlag(true); + } else { + tm.setTile(p2, tileIdx, TileMapItem.FLAG_RIGHT); + } + } + } + + // adding right border? + p2.x = p.x + 1; p2.y = p.y; + if (p2.x < width && (p2.x == regionBounds.x + regionBounds.width || !locations.contains(p2))) { + final TileInfo ti = wedInfo.getTile(p2); + final int tileIdx = (isSecondary && ti.tileSecondary >= 0) ? ti.tileSecondary : ti.getPrimaryTileIndex(framePrimary); + if (tileIdx >= 0) { + final TileMapItem tmi = tm.getTile(p2); + if (tmi != null) { + tmi.setLeftFlag(true); + } else { + tm.setTile(p2, tileIdx, TileMapItem.FLAG_LEFT); + } + } + } + + // adding top border? + p2.x = p.x; p2.y = p.y - 1; + if (p2.y >= 0 && (p2.y == regionBounds.y - 1 || !locations.contains(p2))) { + final TileInfo ti = wedInfo.getTile(p2); + final int tileIdx = (isSecondary && ti.tileSecondary >= 0) ? ti.tileSecondary : ti.getPrimaryTileIndex(framePrimary); + if (tileIdx >= 0) { + final TileMapItem tmi = tm.getTile(p2); + if (tmi != null) { + tmi.setBottomFlag(true); + } else { + tm.setTile(p2, tileIdx, TileMapItem.FLAG_BOTTOM); + } + } + } + + // adding bottom border? + p2.x = p.x; p2.y = p.y + 1; + if (p2.y < height && (p2.y == regionBounds.y + regionBounds.height || !locations.contains(p2))) { + final TileInfo ti = wedInfo.getTile(p2); + final int tileIdx = (isSecondary && ti.tileSecondary >= 0) ? ti.tileSecondary : ti.getPrimaryTileIndex(framePrimary); + if (tileIdx >= 0) { + final TileMapItem tmi = tm.getTile(p2); + if (tmi != null) { + tmi.setTopFlag(true); + } else { + tm.setTile(p2, tileIdx, TileMapItem.FLAG_TOP); + } + } + } + + // adding top-left corner? + p2.x = p.x - 1; p2.y = p.y - 1; + if (p2.x >= 0 && p2.y >= 0 && + ((p2.x == regionBounds.x - 1 && p2.y == regionBounds.y - 1) || !locations.contains(p2))) { + TileMapItem tmi1 = tm.getTile(p.x, p.y - 1); + TileMapItem tmi2 = tm.getTile(p.x - 1, p.y); + if (tmi1 != null && tmi1.isBottomFlag() && tmi2 != null && tmi2.isRightFlag()) { + final TileInfo ti = wedInfo.getTile(p2); + final int tileIdx = (isSecondary && ti.tileSecondary >= 0) ? ti.tileSecondary : ti.getPrimaryTileIndex(framePrimary); + if (tileIdx >= 0) { + final TileMapItem tmi = tm.getTile(p2); + if (tmi != null) { + tmi.setBottomRightFlag(true); + } else { + tm.setTile(p2, tileIdx, TileMapItem.FLAG_BOTTOM_RIGHT); + } + } + } + } + + // adding top-right corner? + p2.x = p.x + 1; p2.y = p.y - 1; + if (p2.x < width && p2.y >= 0 && + ((p2.x == regionBounds.x + regionBounds.width && p2.y == regionBounds.y - 1) || !locations.contains(p2))) { + TileMapItem tmi1 = tm.getTile(p.x, p.y - 1); + TileMapItem tmi2 = tm.getTile(p.x + 1, p.y); + if (tmi1 != null && tmi1.isBottomFlag() && tmi2 != null && tmi2.isLeftFlag()) { + final TileInfo ti = wedInfo.getTile(p2); + final int tileIdx = (isSecondary && ti.tileSecondary >= 0) ? ti.tileSecondary : ti.getPrimaryTileIndex(framePrimary); + if (tileIdx >= 0) { + final TileMapItem tmi = tm.getTile(p2); + if (tmi != null) { + tmi.setBottomLeftFlag(true); + } else { + tm.setTile(p2, tileIdx, TileMapItem.FLAG_BOTTOM_LEFT); + } + } + } + } + + // adding bottom-left corner? + p2.x = p.x - 1; p2.y = p.y + 1; + if (p2.x >= 0 && p2.y < height && + ((p2.x == regionBounds.x - 1 && p2.y == regionBounds.y + regionBounds.height) || !locations.contains(p2))) { + TileMapItem tmi1 = tm.getTile(p.x, p.y + 1); + TileMapItem tmi2 = tm.getTile(p.x - 1, p.y); + if (tmi1 != null && tmi1.isTopFlag() && tmi2 != null && tmi2.isRightFlag()) { + final TileInfo ti = wedInfo.getTile(p2); + final int tileIdx = (isSecondary && ti.tileSecondary >= 0) ? ti.tileSecondary : ti.getPrimaryTileIndex(framePrimary); + if (tileIdx >= 0) { + final TileMapItem tmi = tm.getTile(p2); + if (tmi != null) { + tmi.setTopRightFlag(true); + } else { + tm.setTile(p2, tileIdx, TileMapItem.FLAG_TOP_RIGHT); + } + } + } + } + + // adding bottom-right corner? + p2.x = p.x + 1; p2.y = p.y + 1; + if (p2.x < width && p2.y < height && + ((p2.x == regionBounds.x + regionBounds.width && p2.y == regionBounds.y + regionBounds.height) || + !locations.contains(p2))) { + TileMapItem tmi1 = tm.getTile(p.x, p.y + 1); + TileMapItem tmi2 = tm.getTile(p.x + 1, p.y); + if (tmi1 != null && tmi1.isTopFlag() && tmi2 != null && tmi2.isLeftFlag()) { + final TileInfo ti = wedInfo.getTile(p2); + final int tileIdx = (isSecondary && ti.tileSecondary >= 0) ? ti.tileSecondary : ti.getPrimaryTileIndex(framePrimary); + if (tileIdx >= 0) { + final TileMapItem tmi = tm.getTile(p2); + if (tmi != null) { + tmi.setTopLeftFlag(true); + } else { + tm.setTile(p2, tileIdx, TileMapItem.FLAG_TOP_LEFT); + } + } + } + } + } + } + + // adding page map + if (!tm.isEmpty()) { + retVal.add(tm); + } + + // updating x0 and y0 + x0 += numTilesX; + if (x0 >= tileBounds.width) { + x0 = 0; + y0 += numTilesY; + } + regionBounds.x = tileBounds.x + x0; + regionBounds.y = tileBounds.y + y0; + } + + return retVal; + } + + /** + * Creates a new PVRZ files based on the given parameters. + * + * @param config {@link Config} instance with global conversion parameters. + * @param pvrzFile {@link Path} of the PVRZ file. + * @param tileMaps List of {@link TileMap} instances with tiles for this pvrz texture. + * @param width Texture width, in pixels. + * @param height Texture height, in pixels. + * @throws Exception if the pvrz file could not be created. + */ + private static void createPvrz(Config config, Path pvrzFile, List tileMaps, int width, int height) + throws Exception { + Objects.requireNonNull(config, "Configuration instance is null"); + Objects.requireNonNull(pvrzFile, "PVRZ file path is null"); + Objects.requireNonNull(tileMaps, "Tile map list is null"); + if (width < 64 || height < 64 || width > 1024 || height > 1024) { + throw new IllegalArgumentException("Unsupported texture size (width=" + width + ", height=" + height + ")"); + } + if (tileMaps.isEmpty()) { + System.err.println("PVRZ creation: No tile maps available for " + pvrzFile.getFileName()); + } + + // generating texture image + final BufferedImage texture = ColorConvert.createCompatibleImage(width, height, true); + Graphics2D g = texture.createGraphics(); + g.setComposite(AlphaComposite.Src); + try { + g.setBackground(Color.BLACK); + g.setColor(Color.BLACK); + g.fillRect(0, 0, texture.getWidth(), texture.getHeight()); + + for (final TileMap tileMap : tileMaps) { + renderTextureTiles(config, g, tileMap); + } + } finally { + g.dispose(); + g = null; + } + + // compressing to DXT1 + final DxtEncoder.DxtType dxtType = DxtEncoder.DxtType.DXT1; + final int pvrCode = 7; // PVR code for DXT1 + final int[] textureData = ((DataBufferInt) texture.getRaster().getDataBuffer()).getData(); + try { + // compressing image data + final byte[] output = new byte[DxtEncoder.calcImageSize(texture.getWidth(), texture.getHeight(), dxtType)]; + DxtEncoder.encodeImage(textureData, texture.getWidth(), texture.getHeight(), output, dxtType, + config.isMultithreaded()); + byte[] header = ConvertToPvrz.createPVRHeader(texture.getWidth(), texture.getHeight(), pvrCode); + byte[] pvrz = new byte[header.length + output.length]; + System.arraycopy(header, 0, pvrz, 0, header.length); + System.arraycopy(output, 0, pvrz, header.length, output.length); + pvrz = Compressor.compress(pvrz, 0, pvrz.length, true); + + // writing pvrz file + try (BufferedOutputStream bos = new BufferedOutputStream(Files.newOutputStream(pvrzFile))) { + bos.write(pvrz); + } + } catch (Exception e) { + try { + Files.delete(pvrzFile); + } catch (IOException e2) { + e2.printStackTrace(); + } + throw e; + } + } + + /** + * Draws tiles from a tile map to a graphics object. + * + * @param config {@link Config} instance with global conversion parameters. + * @param g {@link Graphics2D} handle of the texture image. + * @param tileMap {@link TileMap} instance with tile layout information. + * @throws Exception if the tile map could not be rendered. + */ + private static void renderTextureTiles(Config config, Graphics2D g, TileMap tileMap) throws Exception { + Objects.requireNonNull(config); + Objects.requireNonNull(g); + Objects.requireNonNull(tileMap); + + // evaluating overlay conversion mode + final OverlayConversion conversionMode; + if (config.getOverlayConversion() != OverlayConversion.NONE && tileMap.getWedInfo() != null && + tileMap.getWedInfo().hasWedResource()) { + conversionMode = config.getOverlayConversion(); + } else { + conversionMode = OverlayConversion.NONE; + } + + BufferedImage tileImg = ColorConvert.createCompatibleImage(Config.TILE_SIZE, Config.TILE_SIZE, true); + final List locations = tileMap.getAllTilePositions(false); + for (final Point p : locations) { + final TileMapItem tmi = tileMap.getTile(p); + // preparations + if (tmi != null) { + config.getDecoder().getTile(tmi.getIndex(), tileImg); + } else { + System.err.println("No tile available at " + p); + continue; + } + + // overlay conversion + if (conversionMode.isImplemented()) { + final Point tileLocation = config.wedInfo.getTileLocation(tmi.getIndex()); + if (tileLocation != null) { + final int priTileIdx = tileLocation.y * config.wedInfo.getWidth() + tileLocation.x; + final TileInfo tileInfo = config.wedInfo.getTile(priTileIdx); + tileImg = conversionMode.getConverter().convert(tmi.getIndex(), tileImg, config.getDecoder(), tileInfo); + } + } + + // rendering tiles + final int borderSize = config.getBorderSize(); + for (int flag = 0; flag <= TileMapItem.FLAG_ALL; flag++) { + if (tmi.isFlag(flag)) { + final int x1, y1, x2, y2; + switch (flag) { + case TileMapItem.FLAG_TOP: // rendering only top rows of pixels + x1 = 0; y1 = 0; x2 = Config.TILE_SIZE; y2 = borderSize; + break; + case TileMapItem.FLAG_BOTTOM: // rendering only bottom rows of pixels + x1 = 0; y1 = Config.TILE_SIZE - borderSize; x2 = Config.TILE_SIZE; y2 = Config.TILE_SIZE; + break; + case TileMapItem.FLAG_LEFT: // rendering only left columns of pixels + x1 = 0; y1 = 0; x2 = borderSize; y2 = Config.TILE_SIZE; + break; + case TileMapItem.FLAG_RIGHT: // rendering only right columns of pixels + x1 = Config.TILE_SIZE - borderSize; y1 = 0; x2 = Config.TILE_SIZE; y2 = Config.TILE_SIZE; + break; + case TileMapItem.FLAG_TOP_LEFT: // rendering only top left rectangle of pixels + x1 = 0; y1 = 0; x2 = borderSize; y2 = borderSize; + break; + case TileMapItem.FLAG_TOP_RIGHT: // rendering only top right rectangle of pixels + x1 = Config.TILE_SIZE - borderSize; y1 = 0; x2 = Config.TILE_SIZE; y2 = borderSize; + break; + case TileMapItem.FLAG_BOTTOM_LEFT: // rendering only bottom left rectangle of pixels + x1 = 0; y1 = Config.TILE_SIZE - borderSize; x2 = borderSize; y2 = Config.TILE_SIZE; + break; + case TileMapItem.FLAG_BOTTOM_RIGHT: // rendering only bottom right rectangle of pixels + x1 = Config.TILE_SIZE - borderSize; y1 = Config.TILE_SIZE - borderSize; + x2 = Config.TILE_SIZE; y2 = Config.TILE_SIZE; + break; + case TileMapItem.FLAG_ALL: // rendering full tile + x1 = 0; y1 = 0; x2 = Config.TILE_SIZE; y2 = Config.TILE_SIZE; + break; + default: // just added for completeness + x1 = y1 = x2 = y2 = 0; + break; + } + g.drawImage(tileImg, tmi.getX() + x1, tmi.getY() + y1, tmi.getX() + x2, tmi.getY() + y2, x1, y1, x2, y2, null); + } + } + } + } + + /** + * Creates a new TIS file based on the given parameters. + * + * @param config {@link Config} instance with global conversion parameters. + * @param tileList List of {@link ConvertToTis.TileEntry} structures with tile information. + * @throws Exception if the tis file could not be created. + */ + private static void createPvrzTis(Config config, List tileList) throws Exception { + Objects.requireNonNull(config); + Objects.requireNonNull(tileList); + if (tileList.isEmpty()) { + System.out.println(getErrorMessage(config, "Tile list is empty")); + } + + try (BufferedOutputStream bos = new BufferedOutputStream(Files.newOutputStream(config.getTisFile()))) { + final byte[] header = new byte[24]; + System.arraycopy("TIS V1 ".getBytes(), 0, header, 0, 8); + // TODO: use different tile count source when implementing overlay conversion modes that change tileset layout + DynamicArray.putInt(header, 0x08, config.getDecoder().getTileCount()); + DynamicArray.putInt(header, 0x0c, 0x0c); + DynamicArray.putInt(header, 0x10, 0x18); + DynamicArray.putInt(header, 0x14, 0x40); + bos.write(header); + + final ConvertToTis.TileEntry defEntry = new ConvertToTis.TileEntry(-1, -1, 0, 0); + final Map tileMap = + tileList.stream().collect(Collectors.toMap(te -> te.tileIndex, Function.identity())); + for (int idx = 0, numTiles = config.getDecoder().getTileCount(); idx < numTiles; idx++) { + final ConvertToTis.TileEntry tileEntry = tileMap.getOrDefault(idx, defEntry); + bos.write(DynamicArray.convertInt(tileEntry.page)); + bos.write(DynamicArray.convertInt(tileEntry.x)); + bos.write(DynamicArray.convertInt(tileEntry.y)); + } + } catch (Exception e) { + try { + Files.delete(config.getTisFile()); + } catch (IOException e2) { + e2.printStackTrace(); + } + throw e; + } + } + + /** + * Generates a PVRZ filename with full path from the given parameters. + * + * @param tisFile TIS file path. + * @param page PVRZ texture page index. + * @return A {@link Path} object with the PVRZ texture filename. + */ + private static Path generatePvrzFileName(Path tisFile, int page) { + if (tisFile != null) { + Path path = tisFile.getParent(); + String tisName = tisFile.getFileName().toString(); + int extOfs = tisName.lastIndexOf('.'); + if (extOfs > 0) { + tisName = tisName.substring(0, extOfs); + } + if (Pattern.matches(".{2,7}", tisName)) { + String pvrzName = String.format("%s%s%02d.PVRZ", tisName.substring(0, 1), + tisName.substring(2, tisName.length()), page); + return path.resolve(pvrzName); + } + } + return null; + } + + /** A helper method that creates an error message with TIS filename and given message. */ + private static String getErrorMessage(Config config, String msg) { + String retVal = (config != null && config.tisFile != null) ? config.tisFile.getFileName().toString() + ": " : ""; + if (msg != null) { + retVal += msg; + } else { + retVal += "Error"; + } + return retVal; + } + + /** A default overlay tile converter routine that does nothing. */ + private static final OverlayTileConverter overlayConverterDefault = (tileIndex, tileImage, decoder, tileInfo) -> tileImage; + + /** Overlay tile conversion for {@link OverlayConversion#BG1_TO_BGEE}. */ + private static final OverlayTileConverter overlayConverterBG1toBGEE = (tileIndex, tileImage, decoder, tileInfo) -> { + throw new UnsupportedOperationException("Not implemented yet"); + }; + + /** Overlay tile conversion for {@link OverlayConversion#BG1_TO_BG2EE}. */ + private static final OverlayTileConverter overlayConverterBG1toBG2EE = (tileIndex, tileImage, decoder, tileInfo) -> { + throw new UnsupportedOperationException("Not implemented yet"); + }; + + /** Overlay tile conversion for {@link OverlayConversion#BG2_TO_BGEE}. */ + private static final OverlayTileConverter overlayConverterBG2toBGEE = (tileIndex, tileImage, decoder, tileInfo) -> { + throw new UnsupportedOperationException("Not implemented yet"); + }; + + /** Overlay tile conversion for {@link OverlayConversion#BG2_TO_BG2EE}. */ + private static final OverlayTileConverter overlayConverterBG2toBG2EE = (tileIndex, tileImage, decoder, tileInfo) -> { + if (tileInfo.flags == 0 || tileInfo.tileSecondary < 0) { + // no overlay conversion needed + return tileImage; + } + + // preparations + final Couple couple = OverlayTileConverter.init(tileIndex, tileImage, decoder, tileInfo); + final int[] priData = ((DataBufferInt) couple.getValue0().getRaster().getDataBuffer()).getData(); + final int[] secData = ((DataBufferInt) couple.getValue1().getRaster().getDataBuffer()).getData(); + final boolean isPrimary = (couple.getValue0() == tileImage); + + // performing overlay conversion + if (isPrimary) { + // removing all pixels on primary tile that are transparent on secondary tile + for (int idx = 0, size = Config.TILE_SIZE * Config.TILE_SIZE; idx < size; idx++) { + if ((secData[idx] & ColorConvert.ALPHA_MASK) == 0) { + priData[idx] = 0; + } + } + } else { + // replacing transparent pixels on secondary tile with pixels from primary tile + // removing all opaque pixels on secondary tile + for (int idx = 0, size = Config.TILE_SIZE * Config.TILE_SIZE; idx < size; idx++) { + secData[idx] = ((secData[idx] & ColorConvert.ALPHA_MASK) == 0) ? priData[idx] : 0; + } + } + return tileImage; + }; + + /** Overlay tile conversion for {@link OverlayConversion#BGEE_TO_BG1}. */ + private static final OverlayTileConverter overlayConverterBGEEtoBG1 = (tileIndex, tileImage, decoder, tileInfo) -> { + throw new UnsupportedOperationException("Not implemented yet"); + }; + + /** Overlay tile conversion for {@link OverlayConversion#BGEE_TO_BG2}. */ + private static final OverlayTileConverter overlayConverterBGEEtoBG2 = (tileIndex, tileImage, decoder, tileInfo) -> { + throw new UnsupportedOperationException("Not implemented yet"); + }; + + /** Overlay tile conversion for {@link OverlayConversion#BG2EE_TO_BG1}. */ + private static final OverlayTileConverter overlayConverterBG2EEtoBG1 = (tileIndex, tileImage, decoder, tileInfo) -> { + throw new UnsupportedOperationException("Not implemented yet"); + }; + + /** Overlay tile conversion for {@link OverlayConversion#BG2EE_TO_BG2}. */ + private static final OverlayTileConverter overlayConverterBG2EEtoBG2 = (tileIndex, tileImage, decoder, tileInfo) -> { + if (tileInfo.flags == 0 || tileInfo.tileSecondary < 0) { + // no overlay conversion needed + return tileImage; + } + + // preparations + final Couple couple = OverlayTileConverter.init(tileIndex, tileImage, decoder, tileInfo); + final int[] priData = ((DataBufferInt) couple.getValue0().getRaster().getDataBuffer()).getData(); + final int[] secData = ((DataBufferInt) couple.getValue1().getRaster().getDataBuffer()).getData(); + final boolean isPrimary = (couple.getValue0() == tileImage); + + // performing overlay conversion + if (isPrimary) { + // replacing transparent pixels on primary tile with pixels from secondary tile + for (int idx = 0, size = Config.TILE_SIZE * Config.TILE_SIZE; idx < size; idx++) { + if ((priData[idx] & ColorConvert.ALPHA_MASK) == 0) { + priData[idx] = secData[idx]; + } + } + } else { + // replacing transparent pixels on secondary tile with pixels from primary tile + // removing all opaque pixels on secondary tile; + for (int idx = 0, size = Config.TILE_SIZE * Config.TILE_SIZE; idx < size; idx++) { + secData[idx] = ((secData[idx] & ColorConvert.ALPHA_MASK) == 0) ? priData[idx] : 0; + } + } + return tileImage; + }; + + /** A default overlay map updater routine that does nothing. */ + private static final OverlayMapUpdater overlayUpdaterDefault = wedInfo -> {}; + + /** + * Modifies the tileset map to account for the overlay tile changes. + * Corresponds with {@link OverlayConversion#BG1_TO_BGEE}. + * + * @param wedInfo {@link WedInfo} instance with tile mappings. + * @throws Exception If the conversion could not be performed. + */ + private static final OverlayMapUpdater overlayUpdaterBG1toBGEE = wedInfo -> { + throw new UnsupportedOperationException("Not implemented yet"); + }; + + /** + * Modifies the tileset map to account for the overlay tile changes. + * Corresponds with {@link OverlayConversion#BG1_TO_BG2EE}. + * + * @param wedInfo {@link WedInfo} instance with tile mappings. + * @throws Exception If the conversion could not be performed. + */ + private static final OverlayMapUpdater overlayUpdaterBG1toBG2EE = wedInfo -> { + throw new UnsupportedOperationException("Not implemented yet"); + }; + + /** + * Modifies the tileset map to account for the overlay tile changes. + * Corresponds with {@link OverlayConversion#BG2_TO_BGEE}. + * + * @param wedInfo {@link WedInfo} instance with tile mappings. + * @throws Exception If the conversion could not be performed. + */ + private static final OverlayMapUpdater overlayUpdaterBG2toBGEE = wedInfo -> { + throw new UnsupportedOperationException("Not implemented yet"); + }; + + /** + * Modifies the tileset map to account for the overlay tile changes. + * Corresponds with {@link OverlayConversion#BGEE_TO_BG1}. + * + * @param wedInfo {@link WedInfo} instance with tile mappings. + * @throws Exception If the conversion could not be performed. + */ + private static final OverlayMapUpdater overlayUpdaterBGEEtoBG1 = wedInfo -> { + throw new UnsupportedOperationException("Not implemented yet"); + }; + + /** + * Modifies the tileset map to account for the overlay tile changes. + * Corresponds with {@link OverlayConversion#BGEE_TO_BG2}. + * + * @param wedInfo {@link WedInfo} instance with tile mappings. + * @throws Exception If the conversion could not be performed. + */ + private static final OverlayMapUpdater overlayUpdaterBGEEtoBG2 = wedInfo -> { + throw new UnsupportedOperationException("Not implemented yet"); + }; + + /** + * Modifies the tileset map to account for the overlay tile changes. + * Corresponds with {@link OverlayConversion#BG2EE_TO_BG1}. + * + * @param wedInfo {@link WedInfo} instance with tile mappings. + * @throws Exception If the conversion could not be performed. + */ + private static final OverlayMapUpdater overlayUpdaterBG2EEtoBG1 = wedInfo -> { + throw new UnsupportedOperationException("Not implemented yet"); + }; + + // -------------------------- INNER CLASSES -------------------------- + + /** + * This class stores global parameters for the tileset conversion operation. + */ + public static class Config { + /** Size of a single tile, in pixels. */ + public static final int TILE_SIZE = 64; + + /** Max. supported texture size, in pixels. */ + public static final int MAX_TEXTURE_SIZE = 1024; + + /** Default number of pixel rows/columns used from border tiles. */ + public static final int DEFAULT_BORDER_SIZE = 2; + + /** + * Creates a new {@code Config} instance and initializes it with the given parameters. This method is intended for + * creating Config objects for PVRZ->Palette TIS conversions. + * + * @param tisFile Path of the output TIS file. Files associated with the TIS file are created in the same + * folder as the TIS file. + * @param tileList List of individual tiles as {@link Image} objects. (PVRZ -> Palette conversion only) + * @param decoder {@link TisDecoder} instance with tile information. + * @param wedEntry An optional {@link ResourceEntry} of the associated WED resource. Specify {@code null} + * to autodetect the WED resource. + * @param overlayConversion Specifies how to convert tile overlays. This mode is only considered if the tileset is + * linked to a WED resource. + * @return A fully configured {@link Config} instance. + * @throws NullPointerException if {@code decoder} is {@code null}. + * @throws Exception if parameters could not be initialized. + */ + public static Config createConfigPalette(Path tisFile, List tileList, TisDecoder decoder, + ResourceEntry wedEntry, OverlayConversion overlayConversion) throws Exception { + if (!(Objects.requireNonNull(decoder) instanceof TisV2Decoder)) { + throw new IllegalArgumentException("Unsupported TIS decoder"); + } + return new Config(tisFile, tileList, decoder, wedEntry, 1, 0, 0, 0, 0, 0, false, false, overlayConversion); + } + + /** + * Creates a new {@code Config} instance and initializes it with the given parameters. This method is intended for + * creating Config objects for Palette->PVRZ TIS conversions. + * + * @param tisFile Path of the output TIS file. Files associated with the TIS file are created in the same + * folder as the TIS file. + * @param decoder {@link TisDecoder} instance with tile information. + * @param wedEntry An optional {@link ResourceEntry} of the associated WED resource. Specify {@code null} + * to autodetect the WED resource. + * @param defaultTilesPerRow Number of tiles per rows that is used to lay out tiles if WED information is + * unavailable. + * @param defaultRowCount Number of rows to use if WED information is unavailable. + * @param textureSize Max. dimension of a pvr texture. + * @param pvrzBaseIndex Start index of PVRZ texture file names. Valid range: [0, 99]. Default: 0 + * @param borderSize Size of border tiles, in pixels. + * @param segmentSize Max. size of tile segments to be placed on PVRZ textures, in pixels. + * @param detectBlack Indicates whether black tiles should be replaced with an implicit default. + * @param multithreaded Indicates whether to use multithreading to encode PVRZ textures. + * @param overlayConversion Specifies how to convert tile overlays. This mode is only considered if the tileset is + * linked to a WED resource. + * @return A fully configured {@link Config} instance. + * @throws NullPointerException if {@code decoder} is {@code null}. + * @throws Exception if parameters could not be initialized. + */ + public static Config createConfigPvrz(Path tisFile, TisDecoder decoder, ResourceEntry wedEntry, + int defaultTilesPerRow, int defaultRowCount, int textureSize, int pvrzBaseIndex, int borderSize, int segmentSize, + boolean detectBlack, boolean multithreaded, OverlayConversion overlayConversion) throws Exception { + if (!(Objects.requireNonNull(decoder) instanceof TisV1Decoder)) { + throw new IllegalArgumentException("Unsupported TIS decoder"); + } + return new Config(tisFile, null, decoder, wedEntry, defaultTilesPerRow, defaultRowCount, MAX_TEXTURE_SIZE, + pvrzBaseIndex, borderSize, segmentSize, detectBlack, multithreaded, overlayConversion); + } + + private final List tileList; + private final TisDecoder decoder; + private final WedInfo wedInfo; + private final Path tisFile; + + private int defaultTilesPerRow; + private int defaultRowCount; + private int textureSize; + private int pvrzBaseIndex; + private int borderSize; + private int segmentSize; + private boolean detectBlack; + private boolean multithreaded; + private OverlayConversion overlayConversion; + + /** + * Creates a new {@code Config} object and initializes it with the given parameters. + * + * @param tisFile Path of the output TIS file. Files associated with the TIS file are created in the same + * folder. + * @param tileList List of individual tiles as {@link Image} objects. (PVRZ -> Palette conversion only) + * @param decoder {@link TisDecoder} instance with tile information. + * @param wedEntry An optional {@link ResourceEntry} of the associated WED resource. Specify {@code null} + * to autodetect the WED resource. + * @param defaultTilesPerRow Number of tiles per rows that is used to lay out tiles if WED information is + * unavailable. + * @param defaultRowCount Number of rows to use if WED information is unavailable. + * @param textureSize Max. dimension of a pvr texture. + * @param pvrzBaseIndex Start index of PVRZ texture file names. Valid range: [0, 99]. Default: 0 + * @param borderSize Size of border tiles, in pixels. + * @param segmentSize Max. size of tile segments to be placed on PVRZ textures, in pixels. + * @param detectBlack Indicates whether black tiles should be detected and replaced by a default (PVRZ only). + * @param multithreaded Indicates whether to use multithreading to encode PVRZ textures (PVRZ only). + * @param overlayConversion Specifies how to convert tile overlays. This mode is only considered if the tileset is + * linked to a WED resource. + * @throws NullPointerException if {@code decoder} is {@code null}. + * @throws Exception if parameters could not be initialized. + */ + private Config(Path tisFile, List tileList, TisDecoder decoder, ResourceEntry wedEntry, + int defaultTilesPerRow, int defaultRowCount, int textureSize, int pvrzBaseIndex, int borderSize, int segmentSize, + boolean detectBlack, boolean multithreaded, OverlayConversion overlayConversion) throws Exception { + this.tisFile = Objects.requireNonNull(tisFile, "Tis file path is null"); + this.decoder = Objects.requireNonNull(decoder, "Decoder is null"); + if (this.decoder instanceof TisV2Decoder) { + this.tileList = Objects.requireNonNull(tileList, "Tile list is null"); + } else { + this.tileList = null; + } + this.defaultTilesPerRow = Math.max(1, Math.min(this.decoder.getTileCount(), defaultTilesPerRow)); + setDefaultRowCount(defaultRowCount); + this.textureSize = ensureBinarySize(Math.max(TILE_SIZE, Math.min(MAX_TEXTURE_SIZE, textureSize))); + setPvrzBaseIndex(pvrzBaseIndex); + this.borderSize = Math.max(0, Math.min(TILE_SIZE, borderSize)); + setSegmentSize(segmentSize); + this.detectBlack = detectBlack; + this.multithreaded = multithreaded; + this.overlayConversion = validateOverlayConversion(overlayConversion, this.decoder); + this.wedInfo = initWedInfo(wedEntry); + } + + /** + * Constructs a new {@code Config} with the attributes of the specified {@code Config} argument. + * + * @param config Source {@link Config} instance. + * @throws NullPointerException if {@code config} is {@code null}. + */ + public Config(Config config) throws Exception { + Objects.requireNonNull(config); + this.tisFile = config.tisFile; + this.tileList = config.tileList; + this.decoder = config.decoder; + this.wedInfo = config.wedInfo; + this.defaultTilesPerRow = config.defaultTilesPerRow; + this.defaultRowCount = config.defaultRowCount; + this.textureSize = config.textureSize; + this.borderSize = config.borderSize; + this.segmentSize = config.segmentSize; + this.detectBlack = config.detectBlack; + this.multithreaded = config.multithreaded; + this.overlayConversion = config.overlayConversion; + } + + /** Returns the assigned {@link Path} to the output TIS file. */ + public Path getTisFile() { + return tisFile; + } + + /** Returns a list of individual tiles as {@link Image} objects. It is only available for PVRZ->Palette conversions. */ + public List getTileList() { + return tileList; + } + + /** Returns the assigned {@link TisDecoder} instance. */ + public TisDecoder getDecoder() { + return decoder; + } + + /** + * Returns an associated {@link WedInfo} object with WED details. A virtual {@code WedInfo} object with calculated + * data is returned if no WED resource is available. + */ + public WedInfo getWedInfo() { + return wedInfo; + } + + /** + * Returns the number of tiles per row that can be used to lay out tiles if WED information is unavailable. + * Only relevant for Palette->PVRZ conversion. + */ + public int getDefaultTilesPerRow() { + return defaultTilesPerRow; + } + + /** + * Assigns a new value for default tiles per row. Returns this {@code Config} instance. Only relevant for + * Palette->PVRZ conversion. + */ + public Config setDefaultTilesPerRow(int tilesPerRow) { + this.defaultTilesPerRow = Math.max(1, Math.min(decoder.getTileCount(), tilesPerRow)); + setDefaultRowCount(getDefaultRowCount()); + return this; + } + + /** + * Returns the number of rows in the tilesets that are used if WED information is unavailable. Only relevant for + * Palette->PVRZ conversion. + */ + public int getDefaultRowCount() { + return defaultRowCount; + } + + /** + * Assigns a new value to the default row count. Returns this {@code Config} instance. Only relevant for + * Palette->PVRZ conversion. + */ + public Config setDefaultRowCount(int rowCount) { + if (rowCount < 1) { + this.defaultRowCount = decoder.getTileCount() / this.defaultTilesPerRow; + } else { + final int maxRows = decoder.getTileCount() / this.defaultTilesPerRow; + this.defaultRowCount = Math.max(1, Math.min(maxRows, rowCount)); + } + return this; + } + + /** Returns the assigned texture size, in pixels. Only relevant for Palette->PVRZ conversion. */ + public int getTextureSize() { + return textureSize; + } + + /** + * Assigns a new texture size, in pixels. Returns this {@code Config} instance. Only relevant for Palette->PVRZ + * conversion. + */ + public Config setTextureSize(int textureSize) { + this.textureSize = ensureBinarySize(Math.max(TILE_SIZE, Math.min(MAX_TEXTURE_SIZE, textureSize))); + return this; + } + + /** Returns the max. supported texture size, in pixels. Only relevant for Palette->PVRZ conversion. */ + public int getMaxTextureSize() { + return MAX_TEXTURE_SIZE; + } + + /** Returns the start index for PVRZ texture files. A PVRZ file index cannot exceed 99. Default: 0 */ + public int getPvrzBaseIndex() { + return pvrzBaseIndex; + } + + /** Sets the start index for PVRZ texture files. A PVRZ file index cannot exceed 99. */ + public Config setPvrzBaseIndex(int pvrzBaseIndex) { + this.pvrzBaseIndex = Math.max(0, Math.min(99, pvrzBaseIndex)); + return this; + } + + /** Returns the assigned size of border tiles rows or columns. Only relevant for Palette->PVRZ conversion. */ + public int getBorderSize() { + return borderSize; + } + + /** + * Assigns a new size of rows or columns for border tiles. Returns this {@code Config} instance. Only relevant for + * Palette->PVRZ conversion. + */ + public Config setBorderSize(int borderSize) { + this.borderSize = Math.max(0, Math.min(TILE_SIZE, borderSize)); + setSegmentSize(getSegmentSize()); // segment size may change + return this; + } + + /** Returns the max. size of contiguous tile segments to be placed on PVRZ textures, in pixels. */ + public int getSegmentSize() { + return segmentSize; + } + + /** Sets the max. size of contiguous tile segments, in pixels. */ + public Config setSegmentSize(int segmentSize) { + final int minSize = TILE_SIZE + getBorderSize() * 2; + this.segmentSize = Math.max(minSize, Math.min(MAX_TEXTURE_SIZE, segmentSize)); + return this; + } + + /** + * Returns whether black tiles should be detected and replaced by a default. Only relevant for Palette->PVRZ + * conversion. + */ + public boolean isDetectBlack() { + return detectBlack; + } + + /** + * Sets whether black tiles should be detected and replaced by a default. Only relevant for Palette->PVRZ + * conversion. + */ + public Config setDetectBlack(boolean set) { + this.detectBlack = set; + return this; + } + + /** Returns whether to use multithreading to encode PVRZ textures. Only relevant for Palette->PVRZ conversion. */ + public boolean isMultithreaded() { + return multithreaded; + } + + /** Sets whether to use multithreading to encode PVRZ textures. Only relevant for Palette->PVRZ conversion. */ + public Config setMultithreaded(boolean set) { + this.multithreaded = set; + return this; + } + + /** Returns the assigned overlay tile conversion mode. */ + public OverlayConversion getOverlayConversion() { + return overlayConversion; + } + + /** Assigns a new overlay tile conversion mode. Returns this {@code Config} instance. */ + public Config setOverlayConversion(OverlayConversion conversion) { + this.overlayConversion = validateOverlayConversion(conversion, decoder); + return this; + } + + /** Initializes and returns a new {@link WedInfo} object based on current data. */ + private WedInfo initWedInfo(ResourceEntry wedEntry) throws Exception { + // getting information from associated WED if available + if (wedEntry == null) { + wedEntry = findWed(decoder.getResourceEntry(), true); + } + WedInfo wedInfo = null; + if (wedEntry != null) { + try { + final WedResource wed = (WedResource) ResourceFactory.getResource(wedEntry); + if (wed != null) { + wedInfo = new WedInfo(wed); + } + } catch (Exception e) { + wedInfo = null; + e.printStackTrace(); + } + } + + // generating tileset information if WED is not available + if (wedInfo == null) { + final int numTiles = decoder.getTileCount(); + final int w = defaultTilesPerRow; + int numPriTiles = w * defaultRowCount; + final int h; + if (numTiles - numPriTiles > numPriTiles) { + // ensure that secondary tile count does NOT exceed primary tile count + h = (numTiles / 2 + w - 1) / w; + numPriTiles = w * h; + } else { + h = defaultRowCount; + } + + // Laying out tiles sequentially: left>right, top>bottom + // Secondary tiles are sequentially added to primary tiles + final List tiles = new ArrayList<>(w * h); + for (int i = 0; i < numPriTiles; i++) { + final int priTileIdx = i; + final int secTileIdx = (numPriTiles + i) < numTiles ? numPriTiles + i : -1; + tiles.add(new TileInfo(new int[] { priTileIdx }, secTileIdx, 0)); + } + wedInfo = new WedInfo(w, h, tiles, null); + } + + return wedInfo; + } + + /** + * Returns the given size rounded up to the next power-of-two value. + * No change if given value is already a power of two. + */ + private static int ensureBinarySize(int size) { + for (int n = 1; n < 31; n++) { + final int binSize = 1 << n; + if (size <= binSize) { + return binSize; + } + } + return 1 << 31; + } + + /** + * Checks if the given overlay conversion mode is valid for the specified TIS decoder. + * Returns the default overlay conversion mode {@link OverlayConversion#NONE} if the check fails. + */ + private static OverlayConversion validateOverlayConversion(OverlayConversion convert, TisDecoder decoder) { + OverlayConversion retVal = convert; + if (retVal == null) { + retVal = OverlayConversion.NONE; + } else { + if (decoder instanceof TisV1Decoder) { + switch (retVal) { + case BGEE_TO_BG1: + case BGEE_TO_BG2: + case BG2EE_TO_BG1: + case BG2EE_TO_BG2: + System.err.println("Unsupported overlay conversion mode: " + retVal); + retVal = OverlayConversion.NONE; + break; + default: + } + } else { + switch (retVal) { + case BG1_TO_BGEE: + case BG1_TO_BG2EE: + case BG2_TO_BGEE: + case BG2_TO_BG2EE: + System.err.println("Unsupported overlay conversion mode: " + retVal); + retVal = OverlayConversion.NONE; + break; + default: + } + } + } + return retVal; + } + + @Override + public int hashCode() { + return Objects.hash(borderSize, decoder, defaultRowCount, defaultTilesPerRow, overlayConversion, textureSize, + tisFile); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + Config other = (Config)obj; + return borderSize == other.borderSize && Objects.equals(decoder, other.decoder) + && defaultRowCount == other.defaultRowCount && defaultTilesPerRow == other.defaultTilesPerRow + && overlayConversion == other.overlayConversion && textureSize == other.textureSize + && Objects.equals(tisFile, other.tisFile); + } + + @Override + public String toString() { + return "Config [tisFile=" + tisFile + ", decoder=" + decoder + ", defaultTilesPerRow=" + defaultTilesPerRow + + ", defaultRowCount=" + defaultRowCount + ", textureSize=" + textureSize + ", borderSize=" + borderSize + + ", overlayConversion=" + overlayConversion + "]"; + } + } + + /** + * This class provides convenient access to WED content. + */ + public static class WedInfo { + /** List of tilemap entries from the primary overlay. */ + private final List tiles = new ArrayList<>(); + + /** Door entries consists of indices to {@link TileInfo} items in the {@code tiles} list. */ + private final List doors = new ArrayList<>(); + + private final WedResource wed; + + /** Tileset width, in tiles. */ + private int width; + /** Tileset height, in tiles. */ + private int height; + + /** + * Creates a new {@code WedInfo} structure from a WED resource. + * + * @param wed WED resource. + * @throws Exception if wed data could not be retrieved. + */ + public WedInfo(WedResource wed) throws Exception { + Objects.requireNonNull(wed); + this.wed = wed; + if (((IsNumeric) wed.getAttribute(WedResource.WED_NUM_OVERLAYS)).getValue() <= 0) { + throw new Exception(wed.getResourceEntry().getResourceName() + ": No wed overlays available"); + } + + // loading primary overlay + final Overlay ovl = (Overlay) Objects.requireNonNull(wed.getAttribute(Overlay.WED_OVERLAY + " 0")); + width = ((IsNumeric) ovl.getAttribute(Overlay.WED_OVERLAY_WIDTH)).getValue(); + height = ((IsNumeric) ovl.getAttribute(Overlay.WED_OVERLAY_HEIGHT)).getValue(); + final List tmList = ovl.getFields(Tilemap.class); + for (final StructEntry se : tmList) { + if (se instanceof Tilemap) { + tiles.add(new TileInfo((Tilemap) se)); + } + } + + // loading door tiles + final List doorList = wed.getFields(Door.class); + for (final StructEntry se : doorList) { + if (se instanceof Door) { + final Door door = (Door) se; + int numIndices = ((IsNumeric) door.getAttribute(Door.WED_DOOR_NUM_TILEMAP_INDICES)).getValue(); + final int[] indices = new int[numIndices]; + for (int i = 0; i < numIndices; i++) { + indices[i] = ((IsNumeric) door.getAttribute(Door.WED_DOOR_TILEMAP_INDEX + " " + i)).getValue(); + } + doors.add(indices); + } + } + } + + /** + * Creates a new {@code WedInfo} structure from manual data. + * + * @param width Number of tiles in horizontal direction. + * @param height Number of tiles in vertical direction. + * @param tiles Array of {@link TileInfo} structures with tile information. + * @throws Exception if parameters contain invalid data. + */ + public WedInfo(int width, int height, List tiles, List doors) throws Exception { + Objects.requireNonNull(tiles); + if (width <= 0) { + throw new Exception("Invalid width: " + width); + } + if (height <= 0) { + throw new Exception("Invalid height: " + height); + } + if (tiles.size() < width * height) { + throw new Exception("Invalid number of tile definitions. Expected: " + (width * height) + ", Found: " + tiles.size()); + } + + this.wed = null; + this.width = width; + this.height = height; + this.tiles.addAll(tiles); + if (doors != null) { + this.doors.addAll(doors); + } + } + + /** Returns whether this {@code WedInfo} object is based on a {@link WedResource} instance. */ + public boolean hasWedResource() { + return (wed != null); + } + + /** Returns the linked {@link WedResource} instance. Return value may be {@code null}. */ + public WedResource getWedResource() { + return wed; + } + + /** Returns the horizontal tileset size, in tiles. */ + public int getWidth() { + return width; + } + + /** Returns the vertical tileset size, in tiles. */ + public int getHeight() { + return height; + } + + /** Returns the number of available tiles. */ + public int getTileCount() { + return tiles.size(); + } + + /** + * Returns a {@code OverlayTileInfo} structure with detailed information about the overlay tile at the given index. + * + * @param index Overlay tile index. + * @return {@link TileInfo} structure with detailed information about the requested tile. + * @throws IndexOutOfBoundsException if the tile index is out of bounds. + */ + public TileInfo getTile(int index) throws IndexOutOfBoundsException { + return tiles.get(index); + } + + /** + * Returns a {@code OverlayTileInfo} structure with detailed information about the overlay tile at the given + * tileset location. + * + * @param x X coordinate of the tile in the tileset. + * @param y Y coordinate of the tile in the tileset. + * @return {@link TileInfo} structure with detailed information about the requested tile. + * @throws IndexOutOfBoundsException if a coordinate points to a location outside of the tileset. + */ + public TileInfo getTile(int x, int y) throws IndexOutOfBoundsException { + if (x < 0 || x >= width) { + throw new IndexOutOfBoundsException("X: " + x + ", Width: " + width); + } + if (y < 0 || y >= height) { + throw new IndexOutOfBoundsException("Y: " + y + ", Height: " + height); + } + return tiles.get(y * width + x); + } + + /** + * Returns a {@code OverlayTileInfo} structure with detailed information about the overlay tile at the given + * tileset location. + * + * @param p Location of the tile in the tileset. + * @return {@link TileInfo} structure with detailed information about the requested tile. + * @throws IndexOutOfBoundsException if a coordinate points to a location outside of the tileset. + */ + public TileInfo getTile(Point p) throws IndexOutOfBoundsException { + Objects.requireNonNull(p, "Point is null"); + + if (p.x < 0 || p.x >= width) { + throw new IndexOutOfBoundsException("X: " + p.x + ", Width: " + width); + } + if (p.y < 0 || p.y >= height) { + throw new IndexOutOfBoundsException("Y: " + p.y + ", Height: " + height); + } + return tiles.get(p.y * width + p.x); + } + + /** + * Returns the location of the specified tile on the tile map. Both primary and secondary indices are considered. + * + * @param tileIndex tile index. + * @return {@link Point} of the tile on the tile map. Returns {@code null} if the tile index is not referenced by + * any tile definition. + */ + public Point getTileLocation(int tileIndex) { + if (tileIndex < 0) { + return null; + } + if (tileIndex < tiles.size()) { + return new Point(tileIndex % width, tileIndex / width); + } else { + for (int idx = 0, size = tiles.size(); idx < size; idx++) { + final TileInfo tileInfo = tiles.get(idx); + boolean match = tileInfo.tileSecondary == tileIndex; + if (!match) { + for (final int frame : tileInfo.tilesPrimary) { + match = (frame == tileIndex); + if (match) { + break; + } + } + } + if (match) { + return new Point(idx % width, idx / width); + } + } + } + return null; + } + + /** Returns the number of available doors. */ + public int getDoorCount() { + return doors.size(); + } + + /** + * Returns the number of tilemap indices for the specified door. + * + * @param doorIndex The door index. + * @return Number of tiles referenced by the specified door. + * @throws IndexOutOfBoundsException if the door index is out of bounds. + */ + public int getDoorTileCount(int doorIndex) throws IndexOutOfBoundsException { + return doors.get(doorIndex).length; + } + + /** + * Returns an array of tilemap indices for the specified door. + * + * @param doorIndex The door index. + * @return Array of tilemap indices for the specified door. These indices can be used by {@link #getTile(int)} to + * get detailed tile information. + * @throws IndexOutOfBoundsException if the door index is out of bounds. + */ + public int[] getDoorTileIndices(int doorIndex) throws IndexOutOfBoundsException { + final int[] ar = doors.get(doorIndex); + return Arrays.copyOf(ar, ar.length); + } + + @Override + public int hashCode() { + return Objects.hash(doors, height, tiles, width); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + WedInfo other = (WedInfo)obj; + return Objects.equals(doors, other.doors) && height == other.height && Objects.equals(tiles, other.tiles) + && width == other.width; + } + + @Override + public String toString() { + return "WedInfo [width=" + width + ", height=" + height + ", tiles=" + tiles + ", doors=" + doors + "]"; + } + } + + /** + * This class stores information about a single WED overlay tile. + */ + public static class TileInfo { + /** The primary tile index. First primary tile index for animated tiles. */ + public int tilePrimary; + + /** Complete list of primary tile indices. */ + public int[] tilesPrimary; + + /** Secondary tile index. */ + public int tileSecondary; + + /** Overlay flags. */ + public int flags; + + /** + * Creates a new overlay tile structure from a {@code WedResource} and indices to overlay and tilemap. + * + * @param wed the {@link WedResource}. + * @param overlayIndex WED overlay index. + * @param tileMapIndex Tilemap index in the overlay structure. + * @throws Exception if information could not be retrieved. + */ + public TileInfo(WedResource wed, int overlayIndex, int tileMapIndex) throws Exception { + this((Overlay) Objects.requireNonNull(wed).getAttribute(Overlay.WED_OVERLAY + " " + overlayIndex), tileMapIndex); + } + + /** + * Creates a new overlay tile structure from a WED {@code Overlay} and index to tilemap. + * + * @param ovl the WED {@link Overlay} structure. + * @param tileMapIndex Tilemap index. + * @throws Exception if information could not be retrieved. + */ + public TileInfo(Overlay ovl, int tileMapIndex) throws Exception { + this((Tilemap) Objects.requireNonNull(ovl).getAttribute(Tilemap.WED_TILEMAP + " " + tileMapIndex)); + } + + /** + * Creates a new overlay tile structure from a WED overlay tilemap. + * + * @param tileMap the WED overlay {@link Tilemap} structure. This structure must be a child of a parent + * {@link Overlay} structure. + * @throws Exception if information could not be retrieved. + */ + public TileInfo(Tilemap tileMap) throws Exception { + Objects.requireNonNull(tileMap); + + final Overlay ovl = (Overlay) tileMap.getParent(); + int tileMapIdx = ((IsNumeric) Objects.requireNonNull(tileMap.getAttribute(Tilemap.WED_TILEMAP_TILE_INDEX_PRI))).getValue(); + int count = ((IsNumeric) Objects.requireNonNull(tileMap.getAttribute(Tilemap.WED_TILEMAP_TILE_COUNT_PRI))).getValue(); + if (count <= 0) { + throw new Exception("Tile count should not be zero or negative"); + } + tilesPrimary = new int[count]; + for (int i = 0; i < count; i++) { + tilesPrimary[i] = ((IsNumeric) Objects.requireNonNull( + ovl.getAttribute(Overlay.WED_OVERLAY_TILEMAP_INDEX + " " + (tileMapIdx + i)))).getValue(); + } + tilePrimary = tilesPrimary[0]; + + tileSecondary = ((IsNumeric) Objects.requireNonNull(tileMap.getAttribute(Tilemap.WED_TILEMAP_TILE_INDEX_SEC))).getValue(); + flags = ((IsNumeric) Objects.requireNonNull(tileMap.getAttribute(Tilemap.WED_TILEMAP_DRAW_OVERLAYS))).getValue(); + } + + /** + * Creates a new overlay tile structure and initialize it with manual values. + * + * @param tilesPrimary List of primary tile indices. + * @param tileSecondary Secondary tile index. + * @param flags Overlay flags. + * @throws Exception if {@code tilesPrimary} is empty. + */ + public TileInfo(int[] tilesPrimary, int tileSecondary, int flags) throws Exception { + if (Objects.requireNonNull(tilesPrimary).length == 0) { + throw new Exception("No primary tile indices specified"); + } + this.tilesPrimary = Arrays.copyOf(tilesPrimary, tilesPrimary.length); + this.tilePrimary = this.tilesPrimary[0]; + this.tileSecondary = tileSecondary; + this.flags = flags; + } + + /** + * Returns the primary tile frame the specified tile index is assigned to. + * + * @param index Primary tile index. + * @return Tile frame of the given tile {@code index}. This is {@code 0} for single frame primary tiles, a frame + * within the range {@code [0,tilesPrimary.length]} for animated tiles, and {@code -1} for secondary or + * non-existing tiles. + */ + public int getPrimaryTileFrame(int index) { + int retVal = -1; + for (int i = 0; i < tilesPrimary.length; i++) { + if (tilesPrimary[i] == index) { + retVal = i; + break; + } + } + return retVal; + } + + /** Returns the primary tile index. */ + public int getPrimaryTileIndex() { + return tilePrimary; + } + + /** + * Returns the primary tile index that is assigned to the specified tile frame. + * + * @param frame Frame index of the primary tile. + * @return Index of the primary tile at the given frame. {@code frame} is clamped to the available tile range. + * Always returns the index of the first primary tile if {@code frame} is negative. + */ + public int getPrimaryTileIndex(int frame) { + if (frame >= 0) { + return tilesPrimary[Math.max(0, frame) % tilesPrimary.length]; + } else { + return tilePrimary; + } + } + + /** Returns the secondary tile index. */ + public int getSecondaryTileIndex() { + return tileSecondary; + } + + /** Returns the tile overlay flags. */ + public int getFlags() { + return flags; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + Arrays.hashCode(tilesPrimary); + result = prime * result + Objects.hash(flags, tileSecondary); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + TileInfo other = (TileInfo)obj; + return flags == other.flags && tileSecondary == other.tileSecondary + && Arrays.equals(tilesPrimary, other.tilesPrimary); + } + + @Override + public String toString() { + return "OverlayTile [tilesPrimary=" + Arrays.toString(tilesPrimary) + ", tileSecondary=" + tileSecondary + + ", flags=" + flags + "]"; + } + } + + /** + * This class stores the visual state of a single tile for the {@link TileMap}. It is used by child elements + * in the {@link TileMap} class. + */ + public static class TileMapItem { + /** + * Indicates that the top-most rows of pixels should be considered (y >= 0). + * This flag cannot be combined with {@link #FLAG_ALL}. + */ + public static final int FLAG_TOP = 0; + /** + * Indicates that the bottom-most rows of pixels should be considered (y <= height-1). + * This flag cannot be combined with {@link #FLAG_ALL}. + */ + public static final int FLAG_BOTTOM = 1; + /** + * Indicates that the left-most columns of pixels should be considered (x >= 0). + * This flag cannot be combined with {@link #FLAG_ALL}. + */ + public static final int FLAG_LEFT = 2; + /** + * Indicates that the right-most columns of pixels should be considered (x <= width-1). + * This flag cannot be combined with {@link #FLAG_ALL}. + */ + public static final int FLAG_RIGHT = 3; + /** + * Indicates that the top-left corner should be considered (x >= 0 && y >= 0). + * This flag cannot be combined with {@link #FLAG_ALL}. + */ + public static final int FLAG_TOP_LEFT = 4; + /** + * Indicates that the top-right corner should be considered (x <= width-1 && y >= 0). + * This flag cannot be combined with {@link #FLAG_ALL}. + */ + public static final int FLAG_TOP_RIGHT = 5; + /** + * Indicates that the bottom-left corner should be considered (x >= 0 && y <= height-1). + * This flag cannot be combined with {@link #FLAG_ALL}. + */ + public static final int FLAG_BOTTOM_LEFT = 6; + /** + * Indicates that the top-left corner should be considered (x <= width-1 && y <= height-1). + * This flag cannot be combined with {@link #FLAG_ALL}. + */ + public static final int FLAG_BOTTOM_RIGHT = 7; + /** + * Indicates that the full tile data should be considered. This flag cannot be combined with the other + * {@code FLAG_xxx} constants. + */ + public static final int FLAG_ALL = 8; + + private final BitSet flags = new BitSet(FLAG_ALL + 1); + private final ConvertToTis.TileEntry tileEntry; + + /** + * Creates a {@code TileInfo} structure with an empty tile index. + */ + public TileMapItem() { + this(-1); + } + + public TileMapItem(int tileIndex, int... flags) { + this.tileEntry = new ConvertToTis.TileEntry(tileIndex, -1, 0, 0); + for (final int flag : flags) { + setFlag(flag, true); + } + } + + public TileMapItem(TileMapItem item) { + Objects.requireNonNull(item); + this.tileEntry = new ConvertToTis.TileEntry(item.tileEntry); + for (int i = 0, size = item.flags.size(); i < size; i++) { + if (item.flags.get(i)) { + this.flags.set(i); + } + } + } + + /** Returns the tile index. */ + public int getIndex() { + return this.tileEntry.tileIndex; + } + + /** Sets the tile index value. */ + public TileMapItem setIndex(int index) { + this.tileEntry.tileIndex = index; + return this; + } + + /** Returns the pvrz texture page. */ + public int getPage() { + return this.tileEntry.page; + } + + /** Sets the pvrz texture page. */ + public TileMapItem setPage(int page) { + this.tileEntry.page = page; + return this; + } + + /** Returns the x coordinate of the tile on the pvrz texture. */ + public int getX() { + return this.tileEntry.x; + } + + /** Sets the x coordinate of the tile on the pvrz texture. */ + public TileMapItem setX(int x) { + this.tileEntry.x = x; + return this; + } + + /** Returns the y coordinate of the tile on the pvrz texture. */ + public int getY() { + return this.tileEntry.y; + } + + /** Sets the y coordinate of the tile on the pvrz texture. */ + public TileMapItem setY(int y) { + this.tileEntry.y = y; + return this; + } + + /** + * General method for setting or clearing specific tile flags. + * + * @param flag The flag (see {@code FLAG_xxx} constants). + * @param set Indicates whether the flag should be set or cleared. + */ + public TileMapItem setFlag(int flag, boolean set) { + if (flag >= 0) { + if (set) { + if (flag == FLAG_ALL) { + flags.clear(); + } else { + flags.clear(FLAG_ALL); + } + } + flags.set(flag, set); + } + return this; + } + + /** Unsets all tile flags. */ + public TileMapItem clearFlags() { + flags.clear(); + return this; + } + + /** Returns whether the specified flag is set. */ + public boolean isFlag(int flag) { + return flags.get(flag); + } + + /** Returns whether {@link #FLAG_TOP} is set. */ + public boolean isTopFlag() { + return flags.get(FLAG_TOP); + } + + /** Sets the state of {@link #FLAG_TOP}. */ + public TileMapItem setTopFlag(boolean set) { + flags.set(FLAG_TOP, set); + if (set) { + flags.clear(FLAG_ALL); + } + return this; + } + + /** Returns whether {@link #FLAG_BOTTOM} is set. */ + public boolean isBottomFlag() { + return flags.get(FLAG_BOTTOM); + } + + /** Sets the state of {@link #FLAG_BOTTOM}. */ + public TileMapItem setBottomFlag(boolean set) { + flags.set(FLAG_BOTTOM, set); + if (set) { + flags.clear(FLAG_ALL); + } + return this; + } + + /** Returns whether {@link #FLAG_LEFT} is set. */ + public boolean isLeftFlag() { + return flags.get(FLAG_LEFT); + } + + /** Sets the state of {@link #FLAG_LEFT}. */ + public TileMapItem setLeftFlag(boolean set) { + flags.set(FLAG_LEFT, set); + if (set) { + flags.clear(FLAG_ALL); + } + return this; + } + + /** Returns whether {@link #FLAG_RIGHT} is set. */ + public boolean isRightFlag() { + return flags.get(FLAG_RIGHT); + } + + /** Sets the state of {@link #FLAG_RIGHT}. */ + public TileMapItem setRightFlag(boolean set) { + flags.set(FLAG_RIGHT, set); + if (set) { + flags.clear(FLAG_ALL); + } + return this; + } + + /** Returns whether {@link #FLAG_TOP_LEFT} is set. */ + public boolean isTopLeftFlag() { + return flags.get(FLAG_TOP_LEFT); + } + + /** Sets the state of {@link #FLAG_TOP_LEFT}. */ + public TileMapItem setTopLeftFlag(boolean set) { + flags.set(FLAG_TOP_LEFT, set); + if (set) { + flags.clear(FLAG_ALL); + } + return this; + } + + /** Returns whether {@link #FLAG_TOP_RIGHT} is set. */ + public boolean isTopRightFlag() { + return flags.get(FLAG_TOP_RIGHT); + } + + /** Sets the state of {@link #FLAG_TOP_RIGHT}. */ + public TileMapItem setTopRightFlag(boolean set) { + flags.set(FLAG_TOP_RIGHT, set); + if (set) { + flags.clear(FLAG_ALL); + } + return this; + } + + /** Returns whether {@link #FLAG_BOTTOM_LEFT} is set. */ + public boolean isBottomLeftFlag() { + return flags.get(FLAG_BOTTOM_LEFT); + } + + /** Sets the state of {@link #FLAG_BOTTOM_LEFT}. */ + public TileMapItem setBottomLeftFlag(boolean set) { + flags.set(FLAG_BOTTOM_LEFT, set); + if (set) { + flags.clear(FLAG_ALL); + } + return this; + } + + /** Returns whether {@link #FLAG_BOTTOM_RIGHT} is set. */ + public boolean isBottomRightFlag() { + return flags.get(FLAG_BOTTOM_RIGHT); + } + + /** Sets the state of {@link #FLAG_BOTTOM_RIGHT}. */ + public TileMapItem setBottomRightFlag(boolean set) { + flags.set(FLAG_BOTTOM_RIGHT, set); + if (set) { + flags.clear(FLAG_ALL); + } + return this; + } + + /** Returns whether {@link #FLAG_ALL} is set. */ + public boolean isAllFlag() { + return flags.get(FLAG_ALL); + } + + /** Sets the state of {@link #FLAG_TOP}. If {@code set} is {@code true} then all other flags are cleared. */ + public TileMapItem setAllFlag(boolean set) { + if (set) { + flags.clear(); + } + flags.set(FLAG_TOP, set); + return this; + } + + @Override + public int hashCode() { + return Objects.hash(flags, tileEntry); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + TileMapItem other = (TileMapItem)obj; + return Objects.equals(flags, other.flags) && Objects.equals(tileEntry, other.tileEntry); + } + + @Override + public String toString() { + return "TileInfo [flags=" + flags + ", tileEntry=" + this.tileEntry + "]"; + } + } + + /** + * This class defines the layout of a tileset region on a virtual canvas. + */ + public static class TileMap { + private final HashMap tiles = new HashMap<>(); +// private final TreeMap tiles = +// new TreeMap<>((p1, p2) -> (p1.y < p2.y) ? -1 : ((p1.y > p2.y) ? 1 : (p1.x - p2.x))); + private final Rectangle pageRect = new Rectangle(); + private final WedInfo wedInfo; + + private int pageIndex; + + private Rectangle bounds; + private int boundsHash; + + public TileMap(WedInfo wedInfo) { + this.wedInfo = wedInfo; + this.pageIndex = -1; + } + + /** Returns the associated {@link WedInfo} instance. Returns {@code null} if no {@code WedInfo} object is available. */ + public WedInfo getWedInfo() { + return wedInfo; + } + + /** Returns {@code true} if this tile map contains no tile definitions. */ + public boolean isEmpty() { + return tiles.isEmpty(); + } + + /** Returns the number of defined tile definitions in this tile map. */ + public int size() { + return tiles.size(); + } + + public TileMapItem getTile(int x, int y) { + return getTile(new Point(x, y)); + } + + public TileMapItem getTile(Point p) { + return tiles.get(p); + } + + public TileMapItem setTile(int x, int y, int tileIndex, int... flags) { + return setTile(new Point(x, y), tileIndex, flags); + } + + public TileMapItem setTile(Point p, int tileIndex, int... flags) { + Objects.requireNonNull(p, "Point is null"); + final TileMapItem ti = new TileMapItem(tileIndex, flags); + return tiles.put(new Point(p), ti); + } + + public TileMapItem setTile(Point p, TileMapItem tile) { + Objects.requireNonNull(p, "Point is null"); + Objects.requireNonNull(tile, "Item is null"); + return tiles.put(new Point(p), new TileMapItem(tile)); + } + + /** + * Removes the tile at the specified tileset location and returns it. Returns {@code null} if the location is + * invalid or doesn't contain any tile definitions. + */ + public TileMapItem removeTile(Point p) { + return tiles.remove(p); + } + + /** + * Returns copies of all available tile positions as a list of {@code Point} objects. + * + * @param sorted Indicates whether list entries should be sorted (from top-left to bottom-right.) + * @return {@link List} of {@link Point} objects. + */ + public List getAllTilePositions(boolean sorted) { + final List retVal = new ArrayList<>(tiles.size()); + tiles.keySet().forEach(p -> retVal.add(new Point(p))); + if (sorted) { + retVal.sort((p1, p2) -> (p1.y < p2.y) ? -1 : ((p1.y > p2.y) ? 1 : (p1.x - p2.x))); + } + return retVal; + } + + /** + * Returns the bounds of the whole tile map. + * + * @param ignoreBorders Specifies whether border tiles should be included in the bounding box calculation. + * @return A {@link Rectangle} that includes all tiles of the tile map. + */ + public Rectangle getTileBounds(boolean ignoreBorders) { + if (boundsHash != tiles.hashCode()) { + bounds = null; + } + if (bounds == null) { + if (tiles.isEmpty()) { + bounds = new Rectangle(); + } else { + int minX = Integer.MAX_VALUE; + int maxX = Integer.MIN_VALUE; + int minY = Integer.MAX_VALUE; + int maxY = Integer.MIN_VALUE; + for (final Map.Entry entry : tiles.entrySet()) { + if (!ignoreBorders || entry.getValue().isAllFlag()) { + final Point p = entry.getKey(); + minX = Math.min(minX, p.x); + maxX = Math.max(maxX, p.x); + minY = Math.min(minY, p.y); + maxY = Math.max(maxY, p.y); + } + } + bounds = new Rectangle(minX, minY, maxX - minX + 1, maxY - minY + 1); + } + } + return bounds; + } + + /** + * Returns the pixel-exact bounding box of the whole tilemap, including border tiles. + * + * @param borderSize Number of pixels per row or column from border tiles to add to the bounding box calculation. + * Specify a size of {@code 0} to ignore border tiles completely. + * @return Bounding {@link Rectangle} of the tile map, in pixels. + */ + public Rectangle getPixelBounds(int borderSize) { + final int tileSize = 64; + borderSize = Math.min(tileSize, Math.max(0, borderSize)); + + final Rectangle retVal = new Rectangle(); + int minX = Integer.MAX_VALUE; + int maxX = Integer.MIN_VALUE; + int minY = Integer.MAX_VALUE; + int maxY = Integer.MIN_VALUE; + for (final Map.Entry entry : tiles.entrySet()) { + final Point p = entry.getKey(); // tile coordinate space + final Point p2 = new Point(p.x * tileSize, p.y * tileSize); // pixel coordinate space + final TileMapItem ti = entry.getValue(); + if (ti.isAllFlag()) { + minX = Math.min(minX, p2.x); + maxX = Math.max(maxX, p2.x + tileSize); + minY = Math.min(minY, p2.y); + maxY = Math.max(maxY, p2.y + tileSize); + } + if (ti.isLeftFlag() && borderSize > 0) { + minX = Math.min(minX, p2.x); + maxX = Math.max(maxX, p2.x + borderSize); + minY = Math.min(minY, p2.y); + maxY = Math.max(maxY, p2.y + tileSize); + } + if (ti.isRightFlag() && borderSize > 0) { + minX = Math.min(minX, p2.x + tileSize - borderSize); + maxX = Math.max(maxX, p2.x + tileSize); + minY = Math.min(minY, p2.y); + maxY = Math.max(maxY, p2.y + tileSize); + } + if (ti.isTopFlag() && borderSize > 0) { + minX = Math.min(minX, p2.x); + maxX = Math.max(maxX, p2.x + tileSize); + minY = Math.min(minY, p2.y); + maxY = Math.max(maxY, p2.y + borderSize); + } + if (ti.isBottomFlag() && borderSize > 0) { + minX = Math.min(minX, p2.x); + maxX = Math.max(maxX, p2.x + tileSize); + minY = Math.min(minY, p2.y + tileSize - borderSize); + maxY = Math.max(maxY, p2.y + tileSize); + } + if (ti.isTopLeftFlag() && borderSize > 0) { + minX = Math.min(minX, p2.x); + maxX = Math.max(maxX, p2.x + borderSize); + minY = Math.min(minY, p2.y); + maxY = Math.max(maxY, p2.y + borderSize); + } + if (ti.isTopRightFlag() && borderSize > 0) { + minX = Math.min(minX, p2.x + tileSize - borderSize); + maxX = Math.max(maxX, p2.x + tileSize); + minY = Math.min(minY, p2.y); + maxY = Math.max(maxY, p2.y + borderSize); + } + if (ti.isBottomLeftFlag() && borderSize > 0) { + minX = Math.min(minX, p2.x); + maxX = Math.max(maxX, p2.x + borderSize); + minY = Math.min(minY, p2.y + tileSize - borderSize); + maxY = Math.max(maxY, p2.y + tileSize); + } + if (ti.isBottomRightFlag() && borderSize > 0) { + minX = Math.min(minX, p2.x + tileSize - borderSize); + maxX = Math.max(maxX, p2.x + tileSize); + minY = Math.min(minY, p2.y + tileSize - borderSize); + maxY = Math.max(maxY, p2.y + tileSize); + } + } + + if (minX != Integer.MAX_VALUE && maxX != Integer.MIN_VALUE && + minY != Integer.MAX_VALUE && maxY != Integer.MIN_VALUE) { + retVal.x = minX; + retVal.y = minY; + retVal.width = maxX - minX; + retVal.height = maxY - minY; + } + + return retVal; + } + + /** Returns whether the tile map has border tiles on the left-most side. */ + public boolean hasLeftBorder() { + final Rectangle bounds = getTileBounds(false); + final Point p = new Point(bounds.x, bounds.y); + int retVal = 0; + for (int y = 0; y < bounds.height; y++) { + final TileMapItem tmi = tiles.get(p); + if (tmi != null) { + if (tmi.isRightFlag() || tmi.isTopRightFlag() || tmi.isBottomRightFlag()) { + retVal = Math.max(retVal, 1); + } else if (tmi.isAllFlag()) { + retVal = Math.max(retVal, 2); + break; + } + } + p.y++; + } + return (retVal == 1); + } + + /** Returns whether the tile map has border tiles on the top-most side. */ + public boolean hasTopBorder() { + final Rectangle bounds = getTileBounds(false); + final Point p = new Point(bounds.x, bounds.y); + int retVal = 0; + for (int x = 0; x < bounds.width; x++) { + final TileMapItem tmi = tiles.get(p); + if (tmi != null) { + if (tmi != null && (tmi.isBottomFlag() || tmi.isBottomLeftFlag() || tmi.isBottomRightFlag())) { + retVal = Math.max(retVal, 1); + } else if (tmi.isAllFlag()) { + retVal = Math.max(retVal, 2); + break; + } + } + p.x++; + } + return (retVal == 1); + } + + /** Returns whether the tile map has border tiles on the right-most side. */ + public boolean hasRightBorder() { + final Rectangle bounds = getTileBounds(false); + final Point p = new Point(bounds.x + bounds.width - 1, bounds.y); + int retVal = 0; + for (int y = 0; y < bounds.height; y++) { + final TileMapItem tmi = tiles.get(p); + if (tmi != null) { + if (tmi.isLeftFlag() || tmi.isTopLeftFlag() || tmi.isBottomLeftFlag()) { + retVal = Math.max(retVal, 1); + } else if (tmi.isAllFlag()) { + retVal = Math.max(retVal, 2); + break; + } + } + p.y++; + } + return (retVal == 1); + } + + /** Returns whether the tile map has border tiles on the bottom-most side. */ + public boolean hasBottomBorder() { + final Rectangle bounds = getTileBounds(false); + final Point p = new Point(bounds.x, bounds.y + bounds.height - 1); + int retVal = 0; + for (int x = 0; x < bounds.width; x++) { + final TileMapItem tmi = tiles.get(p); + if (tmi != null) { + if (tmi.isTopFlag() || tmi.isTopLeftFlag() || tmi.isTopRightFlag()) { + retVal = Math.max(retVal, 1); + } else if (tmi.isAllFlag()) { + retVal = Math.max(retVal, 2); + break; + } + } + p.x++; + } + return (retVal == 1); + } + + /** Returns the pvrz texture page index. */ + public int getPage() { + return pageIndex; + } + + /** Returns the bounding rectangle of the region on the pvrz texture. */ + public Rectangle getPageRect() { + return pageRect; + } + + /** + * Maps tile of this map to a pvrz texture. + * + * @param pageIndex The texture page index. Specify -1 to invalidate texture mapping data. + * @param pageRect Bounding rectangle of the region on the texture page. + * @param borderSize Size of border tiles to consider. + */ + public void setPageRect(int pageIndex, Rectangle pageRect, int borderSize) { + pageIndex = Math.max(-1, Math.min(99, pageIndex)); + if (pageIndex < 0 || pageRect == null) { + pageRect = new Rectangle(); + } + + this.pageIndex = pageIndex; + this.pageRect.x = pageRect.x; + this.pageRect.y = pageRect.y; + this.pageRect.width = pageRect.width; + this.pageRect.height = pageRect.height; + + final int tileSize = 64; + final Rectangle tileRect = getTileBounds(false); + final int xOfs = hasLeftBorder() ? borderSize - tileSize : 0; + final int yOfs = hasTopBorder() ? borderSize - tileSize : 0; + for (final Map.Entry entry : tiles.entrySet()) { + final Point p = entry.getKey(); + final TileMapItem tmi = entry.getValue(); + final int x = (pageIndex >= 0) ? (pageRect.x + ((p.x - tileRect.x) * tileSize) + xOfs) : 0; + final int y = (pageIndex >= 0) ? (pageRect.y + ((p.y - tileRect.y) * tileSize) + yOfs) : 0; + tmi.setPage(pageIndex).setX(x).setY(y); + } + } + + /** + * Cleans up invalid flags of border tiles. + */ + public void repair() { + // cleaning unneeded border flags + for (final Iterator> iter = tiles.entrySet().iterator(); iter.hasNext();) { + final Map.Entry entry = iter.next(); + final TileMapItem tmi = entry.getValue(); + final Point p = entry.getKey(); + final Point p2 = new Point(); // reused to reduce memory allocations + + if (tmi.isBottomFlag()) { + p2.x = p.x; p2.y = p.y + 1; + final TileMapItem tmi2 = tiles.get(p2); + if (tmi2 == null || !tmi2.isAllFlag()) { + tmi.setBottomFlag(false); + } + } + + if (tmi.isTopFlag()) { + p2.x = p.x; p2.y = p.y - 1; + final TileMapItem tmi2 = tiles.get(p2); + if (tmi2 == null || !tmi2.isAllFlag()) { + tmi.setTopFlag(false); + } + } + + if (tmi.isRightFlag()) { + p2.x = p.x + 1; p2.y = p.y; + final TileMapItem tmi2 = tiles.get(p2); + if (tmi2 == null || !tmi2.isAllFlag()) { + tmi.setRightFlag(false); + } + } + + if (tmi.isLeftFlag()) { + p2.x = p.x - 1; p2.y = p.y; + final TileMapItem tmi2 = tiles.get(p2); + if (tmi2 == null || !tmi2.isAllFlag()) { + tmi.setLeftFlag(false); + } + } + + if (tmi.flags.isEmpty()) { + iter.remove(); + } + } + } + + /** + * Extracts distinct clusters of tiles into separate {@code TileMap} objects. + * + * @return {@link List} of {@link TileMap} objects. List is empty if the current object does not contain any tiles. + */ + public List split() { + final List retVal = new ArrayList<>(); + + final Set locations = new HashSet<>(); + for (final Point key : tiles.keySet()) { + if (!locations.contains(key)) { + final TileMap tm = findContiguousTiles(null, key, locations); + // add only TileMap objects that contain full tile definitions + if (!tm.isEmpty()) { + boolean isAll = false; + for (final Point p : tm.getAllTilePositions(false)) { + final TileMapItem tmi = tm.getTile(p); + if (tmi != null && tmi.isAllFlag()) { + isAll = true; + break; + } + } + if (isAll) { + tm.repair(); + retVal.add(tm); + } + } + } + } + + return retVal; + } + + /** + * Recursive method that finds all contiguous tiles relative to the given {@code Point} and adds them to the + * specified {@code TileMap} object. + * + * @param tileMap {@link TileMap} object to store contiguous tiles in. Will be created if {@code null} is + * specified. + * @param p Tile location to process. + * @param locations Global list of already processed tiles to prevent marking a tile multiple times. + * @return the {@code tileMap} argument. + * @throws NullPointerException if the {@code locations} argument is null. + */ + private TileMap findContiguousTiles(TileMap tileMap, Point p, Set locations) { + if (locations == null) { + throw new NullPointerException("Location set is null"); + } + + if (tileMap == null) { + tileMap = new TileMap(getWedInfo()); + } + + if (locations.contains(p)) { + return tileMap; + } + + final TileMapItem item = getTile(p); + if (item == null) { + return tileMap; + } + + tileMap.setTile(p, item); + if (item.isAllFlag()) { + locations.add(p); + } + + if (item.isAllFlag() || item.isTopFlag()) { + // top + final Point p2 = new Point(p.x, p.y - 1); + findContiguousTiles(tileMap, p2, locations); + } + if (item.isAllFlag() || item.isLeftFlag()) { + // left + final Point p2 = new Point(p.x - 1, p.y); + findContiguousTiles(tileMap, p2, locations); + } + if (item.isAllFlag() || item.isBottomFlag()) { + // bottom + final Point p2 = new Point(p.x, p.y + 1); + findContiguousTiles(tileMap, p2, locations); + } + if (item.isAllFlag() || item.isRightFlag()) { + // right + final Point p2 = new Point(p.x + 1, p.y); + findContiguousTiles(tileMap, p2, locations); + } + + return tileMap; + } + + @Override + public int hashCode() { + return Objects.hash(tiles); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + TileMap other = (TileMap)obj; + return Objects.equals(tiles, other.tiles); + } + + @Override + public String toString() { + return "TileMap [tiles=" + tiles + "]"; + } + } +} diff --git a/src/org/infinity/resource/graphics/TisDecoder.java b/src/org/infinity/resource/graphics/TisDecoder.java index eba119d45..7997938a8 100644 --- a/src/org/infinity/resource/graphics/TisDecoder.java +++ b/src/org/infinity/resource/graphics/TisDecoder.java @@ -7,6 +7,7 @@ import java.awt.Image; import java.io.InputStream; import java.nio.ByteBuffer; +import java.util.Objects; import org.infinity.resource.key.ResourceEntry; import org.infinity.util.io.StreamUtils; @@ -163,6 +164,32 @@ protected void setType(Type type) { this.type = type; } + @Override + public int hashCode() { + return Objects.hash(tisEntry, type); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + TisDecoder other = (TisDecoder)obj; + return Objects.equals(tisEntry, other.tisEntry) && type == other.type; + } + + @Override + public String toString() { + return "TisDecoder [type=" + type + ", tisEntry=" + tisEntry + "]"; + } + + // -------------------------- INNER CLASSES -------------------------- /** A class for providing parsed TIS header information. */ diff --git a/src/org/infinity/resource/graphics/TisResource.java b/src/org/infinity/resource/graphics/TisResource.java index 845639ac0..ec487edcd 100644 --- a/src/org/infinity/resource/graphics/TisResource.java +++ b/src/org/infinity/resource/graphics/TisResource.java @@ -8,14 +8,19 @@ import java.awt.BorderLayout; import java.awt.Color; import java.awt.Component; +import java.awt.Dialog; import java.awt.Dimension; import java.awt.FlowLayout; import java.awt.Graphics2D; +import java.awt.GridBagConstraints; +import java.awt.GridBagLayout; import java.awt.GridLayout; import java.awt.Image; +import java.awt.Insets; import java.awt.Point; -import java.awt.Rectangle; +import java.awt.RenderingHints; import java.awt.Transparency; +import java.awt.Window; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.ItemEvent; @@ -24,42 +29,54 @@ import java.awt.event.KeyListener; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; +import java.awt.event.MouseMotionAdapter; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; +import java.awt.geom.AffineTransform; import java.awt.image.BufferedImage; import java.awt.image.DataBufferInt; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; -import java.io.BufferedOutputStream; import java.io.File; -import java.io.IOException; -import java.io.OutputStream; import java.nio.ByteBuffer; import java.nio.file.Files; +import java.nio.file.InvalidPathException; import java.nio.file.Path; +import java.nio.file.Paths; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; +import java.util.LinkedList; import java.util.List; import java.util.Locale; -import java.util.regex.Pattern; +import java.util.Objects; +import java.util.Vector; +import java.util.function.Supplier; -import javax.imageio.ImageIO; import javax.swing.BorderFactory; +import javax.swing.ButtonGroup; +import javax.swing.ImageIcon; import javax.swing.JButton; import javax.swing.JCheckBox; +import javax.swing.JComboBox; import javax.swing.JComponent; +import javax.swing.JDialog; import javax.swing.JFileChooser; import javax.swing.JLabel; import javax.swing.JMenuItem; import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JPopupMenu; +import javax.swing.JRadioButton; import javax.swing.JScrollPane; import javax.swing.JSlider; +import javax.swing.JTextArea; import javax.swing.JTextField; -import javax.swing.ProgressMonitor; +import javax.swing.KeyStroke; import javax.swing.RootPaneContainer; import javax.swing.SwingConstants; +import javax.swing.SwingUtilities; import javax.swing.SwingWorker; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; @@ -71,10 +88,8 @@ import org.infinity.gui.ButtonPopupMenu; import org.infinity.gui.TileGrid; import org.infinity.gui.ViewFrame; +import org.infinity.gui.ViewerUtil; import org.infinity.gui.WindowBlocker; -import org.infinity.gui.converter.ConvertToPvrz; -import org.infinity.gui.converter.ConvertToTis; -import org.infinity.gui.converter.ConvertToTis.TileEntry; import org.infinity.resource.AbstractStruct; import org.infinity.resource.Closeable; import org.infinity.resource.Profile; @@ -83,19 +98,18 @@ import org.infinity.resource.ResourceFactory; import org.infinity.resource.StructEntry; import org.infinity.resource.ViewableContainer; +import org.infinity.resource.graphics.TisConvert.Status; import org.infinity.resource.key.BIFFResourceEntry; import org.infinity.resource.key.ResourceEntry; -import org.infinity.resource.wed.Door; import org.infinity.resource.wed.IndexNumber; import org.infinity.resource.wed.Overlay; import org.infinity.resource.wed.Tilemap; import org.infinity.resource.wed.WedResource; import org.infinity.search.ReferenceSearcher; -import org.infinity.util.BinPack2D; -import org.infinity.util.DynamicArray; -import org.infinity.util.IntegerHashMap; +import org.infinity.util.DataString; +import org.infinity.util.Debugging; import org.infinity.util.io.FileEx; -import org.infinity.util.io.StreamUtils; +import org.infinity.util.tuples.Couple; /** * This resource describes a tileset. There are currently two variants available: @@ -131,12 +145,6 @@ */ public class TisResource implements Resource, Closeable, Referenceable, ActionListener, ChangeListener, ItemListener, KeyListener, PropertyChangeListener { - private enum Status { - SUCCESS, CANCELLED, ERROR, UNSUPPORTED - } - - private static final Color TRANSPARENT_COLOR = new Color(0, true); - private static final int DEFAULT_COLUMNS = 5; private static final String FMT_TILEINFO_SHOW = "Tile %d: Show PVRZ information..."; @@ -151,6 +159,7 @@ private enum Status { private final JMenuItem miTileInfoShow = new JMenuItem(); private final JMenuItem miTileInfoPvrz = new JMenuItem(); private final JMenuItem miTileInfoWed = new JMenuItem(); + private final List> workers = new ArrayList<>(); private WedResource wedResource; private HashMap wedTileMap; @@ -166,9 +175,6 @@ private enum Status { private JMenuItem miExportPNG; private JPanel panel; // top-level panel of the viewer private RootPaneContainer rpc; - private SwingWorker workerToPalettedTis; - private SwingWorker workerToPvrzTis; - private SwingWorker workerExport; private WindowBlocker blocker; private int defaultWidth; private int lastTileInfoIndex = -1; @@ -187,64 +193,42 @@ public void actionPerformed(ActionEvent event) { } else if (event.getSource() == miExport) { ResourceFactory.exportResource(entry, panel.getTopLevelAncestor()); } else if (event.getSource() == miExportPaletteTis) { - final Path tisFile = getTisFileName(panel.getTopLevelAncestor(), false); - if (tisFile != null) { - blocker = new WindowBlocker(rpc); - blocker.setBlocked(true); - workerToPalettedTis = new SwingWorker() { - @Override - public Status doInBackground() { - Status retVal = Status.ERROR; - try { - retVal = convertToPaletteTis(tisFile, true); - } catch (Exception e) { - e.printStackTrace(); - } - return retVal; - } - }; - workerToPalettedTis.addPropertyChangeListener(this); - workerToPalettedTis.execute(); + try { + final TisConvert.Config config = ConvertTisDialog.show(panel.getTopLevelAncestor(), this); + if (config != null) { + performBackgroundTask(() -> { + Debugging.timerReset(); + Status status = TisConvert.convertToPaletteTis(config, true, panel.getTopLevelAncestor()); + Debugging.timerShow("TIS conversion completed", Debugging.TimeFormat.MILLISECONDS); + return status; + }); + } + } catch (Exception e) { + e.printStackTrace(); + JOptionPane.showMessageDialog(panel.getTopLevelAncestor(), "Tileset conversion: " + e.getMessage(), "Error", + JOptionPane.ERROR_MESSAGE); } } else if (event.getSource() == miExportPvrzTis) { - final Path tisFile = getTisFileName(panel.getTopLevelAncestor(), true); - if (tisFile != null) { - blocker = new WindowBlocker(rpc); - blocker.setBlocked(true); - workerToPvrzTis = new SwingWorker() { - @Override - public Status doInBackground() { - Status retVal = Status.ERROR; - try { - retVal = convertToPvrzTis(tisFile, true); - } catch (Exception e) { - e.printStackTrace(); - } - return retVal; - } - }; - workerToPvrzTis.addPropertyChangeListener(this); - workerToPvrzTis.execute(); + try { + final TisConvert.Config config = ConvertTisDialog.show(panel.getTopLevelAncestor(), this); + if (config != null) { + performBackgroundTask(() -> { + Debugging.timerReset(); + Status status = TisConvert.convertToPvrzTis(config, true, panel.getTopLevelAncestor()); + Debugging.timerShow("TIS conversion completed", Debugging.TimeFormat.MILLISECONDS); + return status; + }); + } + } catch (Exception e) { + e.printStackTrace(); + JOptionPane.showMessageDialog(panel.getTopLevelAncestor(), "Tileset conversion: " + e.getMessage(), "Error", + JOptionPane.ERROR_MESSAGE); } } else if (event.getSource() == miExportPNG) { final Path pngFile = getPngFileName(panel.getTopLevelAncestor()); if (pngFile != null) { - blocker = new WindowBlocker(rpc); - blocker.setBlocked(true); - workerExport = new SwingWorker() { - @Override - public Status doInBackground() { - Status retVal = Status.ERROR; - try { - retVal = exportPNG(pngFile, true); - } catch (Exception e) { - e.printStackTrace(); - } - return retVal; - } - }; - workerExport.addPropertyChangeListener(this); - workerExport.execute(); + performBackgroundTask(() -> TisConvert.exportPNG(tileImages, tileGrid.getTileColumns(), pngFile, true, + panel.getTopLevelAncestor())); } } else if (event.getSource() == miTileInfoShow) { if (!showPvrzInfo(lastTileInfoIndex)) { @@ -343,6 +327,7 @@ public void propertyChange(PropertyChangeEvent event) { if (event.getSource() instanceof SwingWorker) { @SuppressWarnings("unchecked") SwingWorker worker = (SwingWorker) event.getSource(); + workers.remove(worker); if ("state".equals(event.getPropertyName()) && SwingWorker.StateValue.DONE == event.getNewValue()) { if (blocker != null) { blocker.setBlocked(false); @@ -381,36 +366,29 @@ public void propertyChange(PropertyChangeEvent event) { @Override public void close() throws Exception { - if (workerToPalettedTis != null) { - if (!workerToPalettedTis.isDone()) { - workerToPalettedTis.cancel(true); - } - workerToPalettedTis = null; - } - if (workerToPvrzTis != null) { - if (!workerToPvrzTis.isDone()) { - workerToPvrzTis.cancel(true); - } - workerToPvrzTis = null; - } - if (workerExport != null) { - if (!workerExport.isDone()) { - workerExport.cancel(true); + while (!workers.isEmpty()) { + SwingWorker worker = workers.remove(0); + if (worker != null && !worker.isDone()) { + worker.cancel(true); } - workerExport = null; + worker = null; } + if (tileImages != null) { tileImages.clear(); tileImages = null; } + if (tileGrid != null) { tileGrid.clearImages(); tileGrid = null; } + if (decoder != null) { decoder.close(); decoder = null; } + System.gc(); } @@ -527,18 +505,18 @@ public JComponent makeViewer(ViewableContainer container) { miExport = new JMenuItem("original"); miExport.addActionListener(this); if (decoder.getType() == TisDecoder.Type.PVRZ) { - miExportPaletteTis = new JMenuItem("as palette-based TIS"); + miExportPaletteTis = new JMenuItem("as palette-based TIS..."); miExportPaletteTis.addActionListener(this); } else if (decoder.getType() == TisDecoder.Type.PALETTE) { - miExportPvrzTis = new JMenuItem("as PVRZ-based TIS"); + miExportPvrzTis = new JMenuItem("as PVRZ-based TIS..."); miExportPvrzTis.addActionListener(this); } miExportPNG = new JMenuItem("as PNG"); miExportPNG.addActionListener(this); List list = new ArrayList<>(); - if (miExport != null) { - list.add(miExport); + if (miExportPNG != null) { + list.add(miExportPNG); } if (miExportPaletteTis != null) { list.add(miExportPaletteTis); @@ -546,8 +524,8 @@ public JComponent makeViewer(ViewableContainer container) { if (miExportPvrzTis != null) { list.add(miExportPvrzTis); } - if (miExportPNG != null) { - list.add(miExportPNG); + if (miExport != null) { + list.add(miExport); } JMenuItem[] mi = new JMenuItem[list.size()]; for (int i = 0; i < mi.length; i++) { @@ -555,7 +533,7 @@ public JComponent makeViewer(ViewableContainer container) { } ((JButton) buttonPanel.addControl(ButtonPanel.Control.FIND_REFERENCES)).addActionListener(this); ButtonPopupMenu bpmExport = (ButtonPopupMenu) buttonPanel.addControl(ButtonPanel.Control.EXPORT_MENU); - bpmExport.setMenuItems(mi); + bpmExport.setMenuItems(mi, false); // 4. packing all together panel = new JPanel(new BorderLayout()); @@ -568,47 +546,43 @@ public JComponent makeViewer(ViewableContainer container) { // --------------------- End Interface Viewable --------------------- - // Returns detected or guessed number of tiles per row of the current TIS - private int getDefaultTilesPerRow() { - return defaultWidth; + /** + * Returns a read-only list of decoded tiles. + * + * @return {@link List} of tiles as {@link Image} objects. + */ + public List getTileList() { + return Collections.unmodifiableList(tileImages); } - // Returns an output filename for a TIS file - private Path getTisFileName(Component parent, boolean enforceValidName) { - Path retVal = null; - JFileChooser fc = new JFileChooser(ResourceFactory.getExportFilePath().toFile()); - fc.setDialogTitle("Export resource"); - fc.setFileSelectionMode(JFileChooser.FILES_ONLY); - FileNameExtensionFilter filter = new FileNameExtensionFilter("TIS files (*.tis)", "tis"); - fc.addChoosableFileFilter(filter); - fc.setFileFilter(filter); - fc.setSelectedFile(new File(fc.getCurrentDirectory(), getResourceEntry().getResourceName())); - boolean repeat = enforceValidName; - do { - retVal = null; - if (fc.showSaveDialog(parent) == JFileChooser.APPROVE_OPTION) { - retVal = fc.getSelectedFile().toPath(); - if (enforceValidName && !isTisFileNameValid(retVal)) { - JOptionPane.showMessageDialog(parent, "PVRZ-based TIS filenames have to be 2 up to 7 characters long.", - "Error", JOptionPane.ERROR_MESSAGE); - } else { - repeat = false; - } - if (FileEx.create(retVal).exists()) { - final String options[] = { "Overwrite", "Cancel" }; - if (JOptionPane.showOptionDialog(parent, retVal + " exists. Overwrite?", "Export resource", - JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE, null, options, options[0]) != 0) { - retVal = null; - repeat = false; - } + /** + * Returns the {@link TisDecoder} instance for this tileset. + * + * @return {@link TisDecoder} instance. + */ + public TisDecoder getDecoder() { + return decoder; + } + + /** Returns whether the specified PVRZ index can be found in the current TIS resource. */ + public boolean containsPvrzReference(int index) { + boolean retVal = false; + if (index >= 0 && index <= 99) { + if (decoder instanceof TisV2Decoder) { + TisV2Decoder tisv2 = (TisV2Decoder) decoder; + for (int i = 0, count = tisv2.getTileCount(); i < count && !retVal; i++) { + retVal = (tisv2.getPvrzPage(i) == index); } - } else { - repeat = false; } - } while (repeat); + } return retVal; } + // Returns detected or guessed number of tiles per row of the current TIS + private int getDefaultTilesPerRow() { + return defaultWidth; + } + // Returns output filename for a PNG file private Path getPngFileName(Component parent) { Path retVal = null; @@ -639,10 +613,11 @@ private void initTileset() { decoder = TisDecoder.loadTis(entry); if (decoder != null) { - wedResource = loadWedForTis(entry); + wedResource = TisConvert.loadWedForTis(entry, false); initOverlayMap(wedResource); int tileCount = decoder.getTileCount(); - defaultWidth = calcTileWidth(wedResource, tileCount); + final ResourceEntry wedEntry = (wedResource != null) ? wedResource.getResourceEntry() : null; + defaultWidth = TisConvert.calcTilesetWidth(wedEntry, false, tileCount); tileImages = new ArrayList<>(tileCount); for (int tileIdx = 0; tileIdx < tileCount; tileIdx++) { BufferedImage image = ColorConvert.createCompatibleImage(64, 64, Transparency.BITMASK); @@ -760,821 +735,1448 @@ private boolean showPvrzInfo(int tileIndex) { return false; } - // Converts the current PVRZ-based tileset into the old tileset variant. - public Status convertToPaletteTis(Path output, boolean showProgress) { - Status retVal = Status.ERROR; - if (output != null) { - if (tileImages != null && !tileImages.isEmpty()) { - String note = "Converting tile %d / %d"; - int progressIndex = 0, progressMax = decoder.getTileCount(); - ProgressMonitor progress = null; - if (showProgress) { - progress = new ProgressMonitor(panel.getTopLevelAncestor(), "Converting TIS...", - String.format(note, progressIndex, progressMax), 0, progressMax); - progress.setMillisToDecideToPopup(500); - progress.setMillisToPopup(2000); - } - - try (BufferedOutputStream bos = new BufferedOutputStream(Files.newOutputStream(output))) { - retVal = Status.SUCCESS; - - // writing header data - byte[] header = new byte[24]; - System.arraycopy("TIS V1 ".getBytes(), 0, header, 0, 8); - DynamicArray.putInt(header, 8, decoder.getTileCount()); - DynamicArray.putInt(header, 12, 0x1400); - DynamicArray.putInt(header, 16, 0x18); - DynamicArray.putInt(header, 20, 0x40); - bos.write(header); - - // writing tile data - int[] palette = new int[255]; - byte[] tilePalette = new byte[1024]; - byte[] tileData = new byte[64 * 64]; - BufferedImage image = ColorConvert.createCompatibleImage(decoder.getTileWidth(), decoder.getTileHeight(), - Transparency.BITMASK); - IntegerHashMap colorCache = new IntegerHashMap<>(1800); // caching RGBColor -> index - for (int tileIdx = 0; tileIdx < decoder.getTileCount(); tileIdx++) { - colorCache.clear(); - if (progress != null && progress.isCanceled()) { - retVal = Status.CANCELLED; - break; - } - progressIndex++; - if (progress != null && (progressIndex % 100) == 0) { - progress.setProgress(progressIndex); - progress.setNote(String.format(note, progressIndex, progressMax)); - } - - Graphics2D g = image.createGraphics(); - try { - g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC)); - g.setColor(TRANSPARENT_COLOR); - g.fillRect(0, 0, image.getWidth(), image.getHeight()); - g.drawImage(tileImages.get(tileIdx), 0, 0, null); - } finally { - g.dispose(); - g = null; - } - - int[] pixels = ((DataBufferInt) image.getRaster().getDataBuffer()).getData(); - if (ColorConvert.medianCut(pixels, 255, palette, true)) { - // filling palette - // first palette entry denotes transparency - tilePalette[0] = tilePalette[2] = tilePalette[3] = 0; - tilePalette[1] = (byte) 255; - for (int i = 1; i < 256; i++) { - tilePalette[(i << 2) + 0] = (byte) (palette[i - 1] & 0xff); - tilePalette[(i << 2) + 1] = (byte) ((palette[i - 1] >>> 8) & 0xff); - tilePalette[(i << 2) + 2] = (byte) ((palette[i - 1] >>> 16) & 0xff); - tilePalette[(i << 2) + 3] = 0; - colorCache.put(palette[i - 1], (byte) (i - 1)); - } - // filling pixel data - for (int i = 0; i < tileData.length; i++) { - if ((pixels[i] & 0xff000000) == 0) { - tileData[i] = 0; - } else { - Byte palIndex = colorCache.get(pixels[i]); - if (palIndex != null) { - tileData[i] = (byte) (palIndex + 1); - } else { - byte color = (byte) ColorConvert.getNearestColor(pixels[i], palette, 0.0, null); - tileData[i] = (byte) (color + 1); - colorCache.put(pixels[i], color); - } - } - } - } else { - retVal = Status.ERROR; - break; - } - bos.write(tilePalette); - bos.write(tileData); - } - image.flush(); - image = null; - tileData = null; - tilePalette = null; - palette = null; - } catch (Exception e) { - retVal = Status.ERROR; - e.printStackTrace(); - } finally { - if (progress != null) { - progress.close(); - progress = null; - } - } - if (retVal != Status.SUCCESS && FileEx.create(output).isFile()) { + /** + * Performs the given operation in a background task. + * + * @param operation Operation to perform as {@link Supplier} object. + * @return {@link SwingWorker} instance that is used to perform the background operation. + */ + private SwingWorker performBackgroundTask(Supplier operation) { + if (operation != null) { + blocker = new WindowBlocker(rpc); + blocker.setBlocked(true); + final SwingWorker worker = new SwingWorker() { + protected Status doInBackground() throws Exception { + Status retVal = Status.ERROR; try { - Files.delete(output); - } catch (IOException e) { + retVal = operation.get(); + } catch (Exception e) { e.printStackTrace(); } + return retVal; } + }; + workers.add(worker); + worker.addPropertyChangeListener(this); + worker.execute(); + return worker; + } + return null; + } + + // Calculates a Dimension structure with the correct number of columns and rows from the specified arguments + private static Dimension calcGridSize(int imageCount, int colSize) { + if (imageCount >= 0 && colSize > 0) { + int rowSize = imageCount / colSize; + if (imageCount % colSize > 0) { + rowSize++; } + return new Dimension(colSize, Math.max(1, rowSize)); } - return retVal; + return null; } - // Converts the current palette-based tileset into the new PVRZ-based variant. - public Status convertToPvrzTis(Path output, boolean showProgress) { - Status retVal = Status.ERROR; - if (output != null) { - try { - ProgressMonitor progress = null; - if (showProgress) { - progress = new ProgressMonitor(panel.getTopLevelAncestor(), "Converting TIS...", "Preparing TIS", 0, 5); - progress.setMillisToDecideToPopup(0); - progress.setMillisToPopup(0); - } + // -------------------------- INNER CLASSES -------------------------- - // try to get associated WED resource - int numTiles = decoder.getTileCount(); - String tisName = decoder.getResourceEntry().getResourceName().toUpperCase(Locale.ENGLISH); - String wedName = tisName.replaceFirst("\\.TIS$", ".WED"); - WedResource wed = null; - Overlay ovl = null; - try { - if (ResourceFactory.resourceExists(wedName)) { - wed = new WedResource(ResourceFactory.getResourceEntry(wedName)); - if (wed != null) { - ovl = (Overlay) wed.getAttribute(Overlay.WED_OVERLAY + " 0"); - } + private final class PopupListener extends MouseAdapter { + @Override + public void mousePressed(MouseEvent e) { + maybeShowPopup(e); + } + + @Override + public void mouseReleased(MouseEvent e) { + maybeShowPopup(e); + } + + private void maybeShowPopup(MouseEvent e) { + if (e.isPopupTrigger()) { + int index = tileGrid.getTileIndexAt(new Point(e.getX(), e.getY())); + if (index >= 0 && index < decoder.getTileCount()) { + lastTileInfoIndex = index; + + miTileInfoShow.setText(String.format(FMT_TILEINFO_SHOW, lastTileInfoIndex)); + miTileInfoShow.setVisible(decoder.getType() == TisDecoder.Type.PVRZ); + + miTileInfoPvrz.setText(String.format(FMT_TILEINFO_PVRZ, lastTileInfoIndex)); + miTileInfoPvrz.setVisible(decoder.getType() == TisDecoder.Type.PVRZ); + + miTileInfoWed.setText(String.format(FMT_TILEINFO_WED, lastTileInfoIndex)); + miTileInfoWed.setVisible(wedTileMap != null && wedTileMap.containsKey(lastTileInfoIndex)); + + if (miTileInfoShow.isVisible() || miTileInfoPvrz.isVisible() || miTileInfoWed.isVisible()) { + menuTileInfo.show(e.getComponent(), e.getX(), e.getY()); } - } catch (Exception e) { - wed = null; - ovl = null; - e.printStackTrace(); } + } + } + } - try (BufferedOutputStream bos = new BufferedOutputStream(Files.newOutputStream(output))) { - // writing header data - byte[] header = new byte[24]; - System.arraycopy("TIS V1 ".getBytes(), 0, header, 0, 8); - DynamicArray.putInt(header, 8, numTiles); - DynamicArray.putInt(header, 12, 0x0c); - DynamicArray.putInt(header, 16, 0x18); - DynamicArray.putInt(header, 20, 0x40); - bos.write(header); - - // processing tiles - final BinPack2D.HeuristicRules binPackRule = BinPack2D.HeuristicRules.BOTTOM_LEFT_RULE; - final int pageDim = 16; // 16 tiles a 64x64 pixels - int tisWidth = 1; - if (ovl != null) { - tisWidth = ((IsNumeric) ovl.getAttribute(Overlay.WED_OVERLAY_WIDTH)).getValue(); - } - int tisHeight = (numTiles + tisWidth - 1) / tisWidth; - int numTilesPrimary = numTiles; - if (ovl != null) { - tisWidth = ((IsNumeric) ovl.getAttribute(Overlay.WED_OVERLAY_WIDTH)).getValue(); - tisHeight = ((IsNumeric) ovl.getAttribute(Overlay.WED_OVERLAY_HEIGHT)).getValue(); - numTilesPrimary = tisWidth * tisHeight; - } - boolean[] markedTiles = new boolean[numTiles]; - Arrays.fill(markedTiles, false); - List listRegions = new ArrayList<>(256); - - // divide primary tiles into regions - int pw = (tisWidth + pageDim - 1) / pageDim; - int ph = (tisHeight + pageDim - 1) / pageDim; - for (int py = 0; py < ph; py++) { - int y = py * pageDim; - int h = Math.min(pageDim, tisHeight - y); - for (int px = 0; px < pw; px++) { - int x = px * pageDim; - int w = Math.min(pageDim, tisWidth - x); - - TileRect rect = new TileRect(x, y, w, h, tisWidth, numTiles, markedTiles); - listRegions.add(rect); - } - } + /** + * This class implements a customizable tileset preview. + */ + private static class TisPreview { + /** Default size of a preview tile, in pixels. */ + public static final int DEF_TILE_SIZE = 4; - // defining additional regions from WED door structures - if (wed != null) { - int numDoors = ((IsNumeric) wed.getAttribute(WedResource.WED_NUM_DOORS)).getValue(); - for (int doorIdx = 0; doorIdx < numDoors; doorIdx++) { - // for each door... - Door door = (Door) wed.getAttribute(Door.WED_DOOR + " " + doorIdx); - int numDoorTiles = ((IsNumeric) door.getAttribute(Door.WED_DOOR_NUM_TILEMAP_INDICES)).getValue(); - if (numDoorTiles > 0) { - Point[] doorTiles = new Point[numDoorTiles]; - Arrays.fill(doorTiles, null); - // getting actual tile indices - for (int doorTileIdx = 0; doorTileIdx < numDoorTiles; doorTileIdx++) { - // for each door tilemap... - Point p = new Point(); // x=tilemap, y=tilemap index - int doorTile = ((IsNumeric) door.getAttribute(Door.WED_DOOR_TILEMAP_INDEX + " " + doorTileIdx)) - .getValue(); - p.x = doorTile; - Tilemap tileMap = (Tilemap) ovl.getAttribute(Tilemap.WED_TILEMAP + " " + doorTile); - // we need both primary and secondary tile index - int index = ((IsNumeric) tileMap.getAttribute(Tilemap.WED_TILEMAP_TILE_INDEX_SEC)).getValue(); - if (index > numTilesPrimary) { - // found already! - p.y = index; - doorTiles[doorTileIdx] = p; - } else { - // processing another redirection for getting the primary tile index - index = ((IsNumeric) tileMap.getAttribute(Tilemap.WED_TILEMAP_TILE_INDEX_PRI)).getValue(); - if (index >= 0 && index < numTilesPrimary) { - index = ((IsNumeric) ovl.getAttribute(Overlay.WED_OVERLAY_TILEMAP_INDEX + " " + index)) - .getValue(); - if (index > numTilesPrimary) { - // found! - p.y = index; - doorTiles[doorTileIdx] = p; - } - } - } - } - - int left = Integer.MAX_VALUE, right = Integer.MIN_VALUE; - int top = Integer.MAX_VALUE, bottom = Integer.MIN_VALUE; - boolean initialized = false; - for (Point p : doorTiles) { - if (p != null) { - initialized = true; - left = Math.min(p.x % tisWidth, left); - right = Math.max(p.x % tisWidth, right); - top = Math.min(p.x / tisWidth, top); - bottom = Math.max(p.x / tisWidth, bottom); - } - } - if (initialized) { - // divide into regions in case door tile size exceeds max. texture size - int doorWidth = right - left + 1; - int doorHeight = bottom - top + 1; - pw = (doorWidth + pageDim - 1) / pageDim; - ph = (doorHeight + pageDim - 1) / pageDim; - for (int py = 0; py < ph; py++) { - int y = py * pageDim; - int h = Math.min(pageDim, doorHeight - y); - for (int px = 0; px < pw; px++) { - int x = px * pageDim; - int w = Math.min(pageDim, doorWidth - x); - - TileRect rect = new TileRect(w, h); - for (Point p : doorTiles) { - if (p != null) { - int dx = (p.x % tisWidth) - left; - int dy = (p.x / tisWidth) - top; - if (dx >= x && dx < x + w && dy >= y && dy < y + h && rect.setMarked(dx, dy, p.y)) { - markedTiles[p.y] = true; - } - } - } - listRegions.add(rect); - } - } - } - } - } + /** Default color of the splitter bar. */ + public static final Color DEF_SPLIT_COLOR = Color.RED; - // handling remaining unmarked tiles - for (int idx = 0; idx < markedTiles.length; idx++) { - if (!markedTiles[idx]) { - TileRect rect = new TileRect(1, 1); - rect.setMarked(0, 0, idx); - listRegions.add(rect); - } - } - } + /** Default interpolation mode for creating preview tiles. */ + private static final Object DEF_INTERPOLATION = RenderingHints.VALUE_INTERPOLATION_BILINEAR; - // packing tileset regions - List entryList = new ArrayList<>(numTiles); - List pageList = new ArrayList<>(); - for (TileRect rect : listRegions) { - Dimension space = new Dimension(rect.bounds); - int pageIndex = -1; - Rectangle rectMatch = null; - for (int idx = 0; idx < pageList.size(); idx++) { - BinPack2D packer = pageList.get(idx); - rectMatch = packer.insert(space.width, space.height, binPackRule); - if (rectMatch.height > 0) { - pageIndex = idx; - break; - } - } + /** Max. number of cached image entries to retain. */ + private static final int CACHE_SIZE_MAX = 32; - // create new page? - if (pageIndex < 0) { - BinPack2D packer = new BinPack2D(pageDim, pageDim); - pageList.add(packer); - pageIndex = pageList.size() - 1; - rectMatch = packer.insert(space.width, space.height, binPackRule); - } + private final LinkedList> imageCache = new LinkedList<>(); - // registering tile entries - for (int idx = 0; idx < rect.indices.length; idx++) { - int x = rect.getX(idx); - int y = rect.getY(idx); - ConvertToTis.TileEntry entry; - if (rect.indices[idx] >= 0) { - entry = new ConvertToTis.TileEntry(rect.indices[idx], pageIndex, (rectMatch.x + x) * 64, - (rectMatch.y + y) * 64); - entryList.add(entry); - } - } - } + private final List tiles = new ArrayList<>(); - // writing TIS entries - Collections.sort(entryList, ConvertToTis.TileEntry.CompareByIndex); - for (TileEntry entry : entryList) { - bos.write(DynamicArray.convertInt(entry.page)); - bos.write(DynamicArray.convertInt(entry.x)); - bos.write(DynamicArray.convertInt(entry.y)); - } + private final TisDecoder decoder; - // generating PVRZ files - retVal = writePvrzPages(output, pageList, entryList, progress); - } finally { - if (progress != null) { - progress.close(); - progress = null; - } - } - } catch (Exception e) { - retVal = Status.ERROR; - e.printStackTrace(); - } - if (retVal != Status.SUCCESS && FileEx.create(output).isFile()) { - try { - Files.delete(output); - } catch (IOException e) { - e.printStackTrace(); - } + private int tileSize; + private Color splitColor; + private Object renderingHints; + + /** + * Creates a tileset preview with the following defaults: + *
    + *
  • Tile size: {@link #DEF_TILE_SIZE} pixels
  • + *
  • Split Color: Red
  • + *
  • Interpolation: Bilinear
  • + *
+ * + * @param decoder {@link TisDecoder} instance with tile information. + * @throws Exception If preview tiles could not be created. + */ + public TisPreview(TisDecoder decoder) throws Exception { + this(decoder, DEF_TILE_SIZE, null, null); + } + + /** + * Creates a tileset preview. + * + * @param decoder {@link TisDecoder} instance with tile information. + * @param tileSize Size of the preview tiles, in pixels. + * @param splitColor Color of the splitter that separates primary tiles from secondary tiles. + * @param renderingHints Interpolation mode used for createing preview tiles. + * @throws Exception If preview tiles could not be created. + */ + public TisPreview(TisDecoder decoder, int tileSize, Color splitColor, Object renderingHints) + throws Exception { + this.decoder = Objects.requireNonNull(decoder); + this.tileSize = Math.max(1, Math.min(64, tileSize)); + this.splitColor = (splitColor != null) ? splitColor : DEF_SPLIT_COLOR; + this.renderingHints = validateRenderingHints(renderingHints); + init(); + } + + /** Returns the associated {@link TisDecoder} instance. */ + public TisDecoder getDecoder() { + return decoder; + } + + /** Returns the preview tile size, in pixels. */ + public int getTileSize() { + return tileSize; + } + + /** Sets the preview tile size, in pixels. Forces the preview tiles to be recreated. */ + public TisPreview setTileSize(int tileSize) throws Exception { + tileSize = Math.max(1, Math.min(64, tileSize)); + if (tileSize != this.tileSize) { + this.tileSize = tileSize; + init(); } + return this; } - return retVal; - } - // Converts the tileset into the PNG format. - public Status exportPNG(Path output, boolean showProgress) { - Status retVal = Status.ERROR; - if (output != null) { - if (tileImages != null && !tileImages.isEmpty()) { - int tilesX = tileGrid.getTileColumns(); - int tilesY = tileGrid.getTileRows(); - if (tilesX > 0 && tilesY > 0) { - BufferedImage image = null; - ProgressMonitor progress = null; - if (showProgress) { - progress = new ProgressMonitor(panel.getTopLevelAncestor(), "Exporting TIS to PNG...", "", 0, 2); - progress.setMillisToDecideToPopup(0); - progress.setMillisToPopup(0); - progress.setProgress(0); - } - image = ColorConvert.createCompatibleImage(tilesX * 64, tilesY * 64, Transparency.BITMASK); - Graphics2D g = image.createGraphics(); - for (int idx = 0; idx < tileImages.size(); idx++) { - if (tileImages.get(idx) != null) { - int tx = idx % tilesX; - int ty = idx / tilesX; - g.drawImage(tileImages.get(idx), tx * 64, ty * 64, null); - } - } - g.dispose(); + /** Returns the color of the splitter that separates the primary tile region from secondary tiles. */ + public Color getSplitColor() { + return splitColor; + } - if (progress != null) { - progress.setProgress(1); - } - try (OutputStream os = StreamUtils.getOutputStream(output, true)) { - if (ImageIO.write(image, "png", os)) { - retVal = Status.SUCCESS; +// /** Sets the color of the splitter that separates the primary tile region from secondary tiles. Default: RED */ +// public TisPreview setSplitColor(Color splitColor) { +// this.splitColor = (splitColor != null) ? splitColor : DEF_SPLIT_COLOR; +// return this; +// } + + /** Returns the interpolation mode used for preview tile creation. */ + public Object getRenderingHints() { + return renderingHints; + } + +// /** Sets the interpolation mode for preview tile creation. Forces the preview tiles to be recreated. */ +// public TisPreview setRenderingHints(Object renderingHints) throws Exception { +// renderingHints = validateRenderingHints(renderingHints); +// if (renderingHints != this.renderingHints) { +// this.renderingHints = renderingHints; +// init(); +// } +// return this; +// } + + /** + * Returns a {@code BufferedImage} instance with the tileset laid out with the specified dimension. + * + * @param width Tileset width, in # tiles. + * @param height Tileset height, in # tiles. + * @return A {@link BufferedImage} object with the tileset representation. + */ + public BufferedImage get(int width, int height) { + final int tilesPerRow = Math.max(1, Math.min(getDecoder().getTileCount(), width)); + final int numRows = (getDecoder().getTileCount() + tilesPerRow - 1) / tilesPerRow; + final int splitPos = (height < numRows) ? Math.max(0, height) : -1; // y position of the splitter bar + final BufferedImage image = createImage(tilesPerRow, numRows); + final int[] buffer = ((DataBufferInt) image.getRaster().getDataBuffer()).getData(); + + // rendering tiles according to given parameters + final int pixelsPerRow = tilesPerRow * tileSize; + for (int y = 0; y < numRows; y++) { + for (int x = 0; x < tilesPerRow; x++) { + int tileIdx = y * tilesPerRow + x; + if (tileIdx < tiles.size()) { + final int[] block = tiles.get(tileIdx); + for (int srcOfs = 0, dstOfs = (y * pixelsPerRow + x) * tileSize, dy = 0; + dy < tileSize; + srcOfs += tileSize, dstOfs += pixelsPerRow, dy++) { + for (int dx = 0; dx < tileSize; dx++) { + buffer[dstOfs + dx] = block[srcOfs + dx]; + } } - } catch (IOException e) { - retVal = Status.ERROR; - e.printStackTrace(); - } - if (progress != null && progress.isCanceled()) { - retVal = Status.CANCELLED; - } - if (progress != null) { - progress.close(); - progress = null; - } - } - if (retVal != Status.SUCCESS && FileEx.create(output).isFile()) { - try { - Files.delete(output); - } catch (IOException e) { - e.printStackTrace(); } } } + + // rendering splitter + if (splitPos >= 0) { + final int ofs = splitPos * pixelsPerRow * tileSize; + final int len = pixelsPerRow; + final int colVal = (getSplitColor().getRGB() & 0x00ffffff) | 0xa0000000; // reduce alpha by 25 percent + Arrays.fill(buffer, ofs, ofs + len, colVal); + } + + return image; } - return retVal; - } - /** - * Attempts to retrieve the tileset width, in tiles, from the specified WED resource. Falls back to a value - * based on {@code defTileCount}, if WED information is not available. - * - * @param wed WED resource for this TIS. - * @param defTileCount A an optional tile count that will be used to "guess" the correct number of tiles per row - * if WED information is no available. - * @return Number of tiles per row for the current TIS resource. - */ - private int calcTileWidth(WedResource wed, int defTileCount) { - int retVal = (defTileCount < 9) ? defTileCount : (int) (Math.sqrt(defTileCount) * 1.18); + /** + * Returns a {@code BufferedImage} object matching the given parameters. + * + * @param width Number of tiles per row. + * @param height Number of tile rows. + * @return A new {@link BufferedImage} object. + */ + private BufferedImage createImage(int width, int height) { + BufferedImage retVal = null; - if (wed != null) { - final Overlay ovl = (Overlay) wed.getAttribute(Overlay.WED_OVERLAY + " 0"); - if (ovl != null) { - final int width = ((IsNumeric) ovl.getAttribute(Overlay.WED_OVERLAY_WIDTH)).getValue(); - if (width > 0) { - retVal = width; + width = Math.max(1, width); + height = Math.max(1, height); + final int imageWidth = width * tileSize; + final int imageHeight = height * tileSize; + final Dimension dim = new Dimension(imageWidth, imageHeight); + Couple entry = + imageCache.stream().filter(c -> c.getValue0().equals(dim)).findFirst().orElse(null); + if (entry != null) { + retVal = entry.getValue1(); + // update cache position + imageCache.remove(entry); + imageCache.addLast(entry); + } else { + retVal = ColorConvert.createCompatibleImage(imageWidth, imageHeight, true); + entry = new Couple(dim, retVal); + imageCache.addLast(entry); + // trimming cache + if (imageCache.size() > CACHE_SIZE_MAX) { + imageCache.removeFirst(); } } - } - return retVal; - } + // clearing image content + final Graphics2D g = retVal.createGraphics(); + try { + g.setComposite(AlphaComposite.Src); + g.setColor(ColorConvert.TRANSPARENT_COLOR); + g.fillRect(0, 0, retVal.getWidth(), retVal.getHeight()); + } finally { + g.dispose(); + } - // Generates PVRZ files based on the current TIS resource and the specified parameters - private Status writePvrzPages(Path tisFile, List pageList, List entryList, - ProgressMonitor progress) { - Status retVal = Status.SUCCESS; - DxtEncoder.DxtType dxtType = DxtEncoder.DxtType.DXT1; - int dxtCode = 7; // PVR code for DXT1 - byte[] output = new byte[DxtEncoder.calcImageSize(1024, 1024, dxtType)]; - String note = "Generating PVRZ file %s / %s"; - if (progress != null) { - progress.setMaximum(pageList.size() + 1); - progress.setProgress(1); + return retVal; } - try { - for (int pageIdx = 0; pageIdx < pageList.size(); pageIdx++) { - if (progress != null) { - if (progress.isCanceled()) { - retVal = Status.CANCELLED; - return retVal; - } - progress.setProgress(pageIdx + 1); - progress.setNote(String.format(note, pageIdx + 1, pageList.size())); - } + /** Creates preview tiles from the tileset. */ + private void init() throws Exception { + tiles.clear(); - Path pvrzFile = generatePvrzFileName(tisFile, pageIdx); - BinPack2D packer = pageList.get(pageIdx); - packer.shrinkBin(true); + final AffineTransform xform = AffineTransform.getScaleInstance(tileSize / 64.0, tileSize / 64.0); + final BufferedImage tileImg = ColorConvert.createCompatibleImage(64, 64, true); - // generating texture image - int w = packer.getBinWidth() * 64; - int h = packer.getBinHeight() * 64; - BufferedImage texture = ColorConvert.createCompatibleImage(w, h, true); - Graphics2D g = texture.createGraphics(); - g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC)); + for (int idx = 0, size = getDecoder().getTileCount(); idx < size; idx++) { + getDecoder().getTile(idx, tileImg); + + // rendering downscaled preview tile + final BufferedImage previewImg = ColorConvert.createCompatibleImage(tileSize, tileSize, true); + final Graphics2D g = previewImg.createGraphics(); try { - g.setBackground(new Color(0, true)); - g.setColor(new Color(0, true)); - g.fillRect(0, 0, texture.getWidth(), texture.getHeight()); - for (final ConvertToTis.TileEntry entry : entryList) { - if (entry.page == pageIdx) { - Image tileImg = decoder.getTile(entry.tileIndex); - int dx = entry.x, dy = entry.y; - g.drawImage(tileImg, dx, dy, dx + 64, dy + 64, 0, 0, 64, 64, null); - } - } + g.setComposite(AlphaComposite.Src); + g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, getRenderingHints()); + g.drawImage(tileImg, xform, null); } finally { g.dispose(); - g = null; } - int[] textureData = ((DataBufferInt) texture.getRaster().getDataBuffer()).getData(); - try { - // compressing PVRZ - int outSize = DxtEncoder.calcImageSize(texture.getWidth(), texture.getHeight(), dxtType); - DxtEncoder.encodeImage(textureData, texture.getWidth(), texture.getHeight(), output, dxtType); - byte[] header = ConvertToPvrz.createPVRHeader(texture.getWidth(), texture.getHeight(), dxtCode); - byte[] pvrz = new byte[header.length + outSize]; - System.arraycopy(header, 0, pvrz, 0, header.length); - System.arraycopy(output, 0, pvrz, header.length, outSize); - header = null; - pvrz = Compressor.compress(pvrz, 0, pvrz.length, true); - - // writing PVRZ to disk - try (BufferedOutputStream bos = new BufferedOutputStream(Files.newOutputStream(pvrzFile))) { - bos.write(pvrz); - } catch (IOException e) { - retVal = Status.ERROR; - e.printStackTrace(); - return retVal; - } - pvrz = null; - } catch (Exception e) { - retVal = Status.ERROR; - e.printStackTrace(); - return retVal; - } + // storing preview tile data + final int[] buf = ((DataBufferInt) previewImg.getRaster().getDataBuffer()).getData(); + tiles.add(Arrays.copyOf(buf, buf.length)); } - } finally { - // cleaning up - if (retVal != Status.SUCCESS) { - for (int i = 0; i < pageList.size(); i++) { - Path pvrzFile = generatePvrzFileName(tisFile, i); - if (pvrzFile != null && FileEx.create(pvrzFile).isFile()) { - try { - Files.delete(pvrzFile); - } catch (IOException e) { - e.printStackTrace(); - } - } - } + } + + /** Ensures that only interpolation hints are returned. */ + private Object validateRenderingHints(Object renderingHints) { + if (renderingHints != RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR && + renderingHints != RenderingHints.VALUE_INTERPOLATION_BILINEAR && + renderingHints != RenderingHints.VALUE_INTERPOLATION_BICUBIC) { + renderingHints = DEF_INTERPOLATION; } + return renderingHints; } - return retVal; } - // Generates PVRZ filename with full path from the given parameters - private Path generatePvrzFileName(Path tisFile, int page) { - if (tisFile != null) { - Path path = tisFile.getParent(); - String tisName = tisFile.getFileName().toString(); - int extOfs = tisName.lastIndexOf('.'); - if (extOfs > 0) { - tisName = tisName.substring(0, extOfs); + /** + * Provides a dialog with options to customize tileset conversion settings. + */ + private static class ConvertTisDialog extends JDialog { + private static final String BORDER_SIZE_LABEL_FMT = "%d pixel(s)"; + private static final String SEGMENT_SIZE_LABEL_FMT = "%d pixels"; + private static final String PVRZ_BASE_INDEX_LABEL_FMT = "%d"; + private static final String TILES_PER_ROW_LABEL_FMT = "%d tile(s)"; + private static final String ROW_COUNT_LABEL_FMT = "%d row(s)"; + + private final Listeners listeners = new Listeners(); + private final HashMap helpMap = new HashMap<>(); + + private final TisResource tis; + private final ResourceEntry wedEntry; + + // set to true only if the dialog is closed by clicking on "OK" + private boolean accepted; + + private int defaultTisWidth; + private int defaultTisHeight; + private int overlayMovementType; + + private JLabel lTisFileLabel; + private JLabel lBorderSizeLabel; + private JLabel lBorderSize; + private JLabel lSegmentSizeLabel; + private JLabel lSegmentSize; + private JLabel lPvrzBaseIndexLabel; + private JLabel lPvrzBaseIndex; + private JLabel lOverlayModeLabel; + private JLabel lTilesPerRowLabel; + private JLabel lTilesPerRow; + private JLabel lNumRowsLabel; + private JLabel lNumRows; + private JLabel lPreviewImageLabel; + private JLabel lPreviewImage; + private JLabel lPreviewImageZoomLabel; + private JLabel lHelpLabel; + private JButton bPreviewReset; + private JButton bOk; + private JButton bCancel; + private JButton bTisFile; + private JCheckBox cbRemoveBlack; + private JCheckBox cbMultithreaded; + private JRadioButton rbAuto; + private JRadioButton rbManual; + private JTextField tfTisFile; + private JTextArea taHelp; + private JSlider sBorderSize; + private JSlider sSegmentSize; + private JSlider sPvrzBaseIndex; + private JSlider sTilesPerRow; + private JSlider sNumRows; + private JComboBox> cbOverlayMode; + private JComboBox> cbPreviewImageZoom; + private JPanel panelSubAuto; + private JPanel panelSubManual; + private TisPreview tisPreview; + private JScrollPane spPreviewImage; + private JScrollPane spHelp; + + /** + * Opens a modal dialog and returns a TIS configuration. + * + * @param owner Parent {@link Window} for the dialog. + * @param decoder {@link TisDecoder} of the source tileset. + * @return An initialized {@link TisConvert.Config} object if the user accepts the conversion options. Returns + * {@code null} if the user cancels the operation. + */ + public static TisConvert.Config show(Component owner, TisResource tis) { + Window window = SwingUtilities.getWindowAncestor(owner); + if (window == null) { + window = NearInfinity.getInstance(); } - if (Pattern.matches(".{2,7}", tisName)) { - String pvrzName = String.format("%s%s%02d.PVRZ", tisName.substring(0, 1), - tisName.substring(2, tisName.length()), page); - return path.resolve(pvrzName); + + TisConvert.Config retVal = null; + ConvertTisDialog dlg = null; + try { + dlg = new ConvertTisDialog(window, tis); + dlg.setVisible(true); + retVal = dlg.getConfig(); + } catch (Exception e) { + e.printStackTrace(); + } finally { + if (dlg != null) { + dlg.dispose(); + dlg = null; + } } + + return retVal; + } + + private ConvertTisDialog(Window owner, TisResource tis) { + super(owner, Dialog.ModalityType.APPLICATION_MODAL); + this.tis = Objects.requireNonNull(tis); + this.wedEntry = TisConvert.findWed(tis.entry, true); + init(); } - return null; - } - // Returns true only if TIS filename can be used to generate PVRZ filenames from - public static boolean isTisFileNameValid(Path fileName) { - if (fileName != null) { - String name = fileName.getFileName().toString(); - int extOfs = name.lastIndexOf('.'); - if (extOfs >= 0) { - name = name.substring(0, extOfs); + /** + * Returns a {@link TisConvert.Config} object initialized with the current dialog settings. + * Returns {@code null} if setting are invalid or the user cancelled the dialog. + */ + private TisConvert.Config getConfig() { + TisConvert.Config retVal = null; + if (!accepted) { + return retVal; } - return Pattern.matches(".{2,7}", name); + + try { + final TisDecoder decoder = tis.getDecoder(); + final Path tisFile = getTisPath(); + final List tileList = getTileList(); + final int tilesPerRow = getTilesPerRow(); + final int rowCount = getRowCount(); + final int textureSize = TisConvert.Config.MAX_TEXTURE_SIZE; + final int pvrzBaseIndex = getPvrzBaseIndex(); + final boolean detectBlack = isDetectBlack(); + final boolean multithreaded = isMultithreaded(); + final int borderSize = getBorderSize(); + final int segmentSize = getSegmentSize(); + final TisConvert.OverlayConversion convert = getOverlayConversionMode(); + + if (decoder instanceof TisV1Decoder) { + retVal = TisConvert.Config.createConfigPvrz(tisFile, decoder, wedEntry, tilesPerRow, rowCount, textureSize, + pvrzBaseIndex, borderSize, segmentSize, detectBlack, multithreaded, convert); + } else if (decoder instanceof TisV2Decoder) { + retVal = TisConvert.Config.createConfigPalette(tisFile, tileList, decoder, wedEntry, convert); + } else { + throw new Exception("Conversion not supported"); + } + } catch (Exception e) { + e.printStackTrace(); + } + + return retVal; } - return false; - } - // Attempts to fix the specified filename to make it compatible with the naming scheme of TIS V2 files - public static Path makeTisFileNameValid(Path fileName) { - if (fileName != null && !isTisFileNameValid(fileName)) { - Path path = fileName.getParent(); - String name = fileName.getFileName().toString(); - String ext = ""; - int extOfs = name.lastIndexOf('.'); - if (extOfs >= 0) { - ext = name.substring(extOfs); - name = name.substring(0, extOfs); + private void init() { + // initializing common components + final boolean isTisV1 = tis.decoder instanceof TisV1Decoder; + + // dialog help + final String helpHelp = "Hover the mouse cursor over the dialog elements for help."; + lHelpLabel = new JLabel("Help:"); + lHelpLabel.addMouseMotionListener(listeners.mouseMotion); + helpMap.put(lHelpLabel, helpHelp); + + final int taCols = isTisV1 ? 50 : 30; + taHelp = new JTextArea(helpHelp, 3, taCols); + taHelp.setEditable(false); + taHelp.setLineWrap(true); + taHelp.setWrapStyleWord(true); + taHelp.addMouseMotionListener(listeners.mouseMotion); + helpMap.put(taHelp, helpHelp); + + spHelp = new JScrollPane(taHelp, JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, + JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); + spHelp.setMinimumSize(spHelp.getPreferredSize()); + + // TIS output file + final String helpTisFile = "Path of the output TIS file."; + lTisFileLabel = new JLabel("TIS Output:"); + lTisFileLabel.addMouseMotionListener(listeners.mouseMotion); + helpMap.put(lTisFileLabel, helpTisFile); + + tfTisFile = new JTextField(); + try { + tfTisFile.setText(Profile.getGameRoot().resolve(tis.entry.getResourceName()).toString()); + } catch (InvalidPathException ex) { + } + tfTisFile.addMouseMotionListener(listeners.mouseMotion); + helpMap.put(tfTisFile, helpTisFile); + + bTisFile = new JButton("Choose..."); + bTisFile.setMnemonic(KeyEvent.VK_C); + bTisFile.addMouseMotionListener(listeners.mouseMotion); + bTisFile.addActionListener(listeners.actionTisFile); + helpMap.put(bTisFile, helpTisFile); + + // overlay conversion modes + final String helpOverlayMode = "Select an overlay conversion mode to correctly convert overlay tiles from one " + + "game to another."; + lOverlayModeLabel = new JLabel("Overlay conversion:"); + lOverlayModeLabel.addMouseMotionListener(listeners.mouseMotion); + helpMap.put(lOverlayModeLabel, helpOverlayMode); + + // initializing overlay mode combobox + cbOverlayMode = initOverlayModeComboBox(true); + cbOverlayMode.addMouseMotionListener(listeners.mouseMotion); + helpMap.put(cbOverlayMode, helpOverlayMode); + + bCancel = new JButton("Cancel"); + bCancel.addActionListener(listeners.actionCancel); + + bOk = new JButton("OK"); + bOk.addActionListener(listeners.actionOk); + bOk.setPreferredSize(new Dimension(bCancel.getPreferredSize())); // buttons should have same size + + // conversion-specific initializations + if (isTisV1) { + initPvrz(); + setTitle("Convert Tileset: Palette -> PVRZ"); + } else { + initPalette(); + setTitle("Convert Tileset: PVRZ -> Palette"); + } + + // continuing common initializations + pack(); + setMinimumSize(getPreferredSize()); + setLocationRelativeTo(getParent()); + bOk.requestFocus(); + + // default action for ENTER key + setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE); + getRootPane().setDefaultButton(bOk); + + // default action for ESCAPE key + getRootPane().registerKeyboardAction(listeners.actionCancel, KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), + JComponent.WHEN_IN_FOCUSED_WINDOW); + + addWindowListener(listeners.windowDialog); + } + + private void initPalette() { + // laying out components on dialog + final GridBagConstraints c = new GridBagConstraints(); + + // top panel (tis file) + final JPanel panelTop = new JPanel(new GridBagLayout()); + ViewerUtil.setGBC(c, 0, 0, 1, 1, 0.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.NONE, + new Insets(0, 0, 0, 8), 0, 0); + panelTop.add(lTisFileLabel, c); + ViewerUtil.setGBC(c, 1, 0, 1, 1, 1.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.HORIZONTAL, + new Insets(0, 0, 0, 8), 0, 0); + panelTop.add(tfTisFile, c); + ViewerUtil.setGBC(c, 2, 0, 1, 1, 0.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.NONE, + new Insets(0, 0, 0, 0), 0, 0); + panelTop.add(bTisFile, c); + + // sub-panel for overlay conversion + final JPanel panelSubOverlay = new JPanel(new GridBagLayout()); + ViewerUtil.setGBC(c, 0, 0, 1, 1, 0.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.NONE, + new Insets(0, 0, 0, 8), 0, 0); + panelSubOverlay.add(lOverlayModeLabel, c); + ViewerUtil.setGBC(c, 1, 0, 1, 1, 0.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.NONE, + new Insets(0, 0, 0, 0), 0, 0); + panelSubOverlay.add(cbOverlayMode, c); + ViewerUtil.setGBC(c, 2, 0, 1, 1, 1.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.HORIZONTAL, + new Insets(0, 0, 0, 0), 0, 0); + panelSubOverlay.add(new JPanel(), c); + + // help panel + final JPanel panelHelp = new JPanel(new GridBagLayout()); + ViewerUtil.setGBC(c, 0, 0, 1, 1, 0.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.NONE, + new Insets(0, 0, 0, 0), 0, 0); + panelHelp.add(lHelpLabel, c); + ViewerUtil.setGBC(c, 0, 1, 1, 1, 1.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.HORIZONTAL, + new Insets(8, 0, 0, 0), 0, 0); + panelHelp.add(spHelp, c); + + // button panel + final JPanel panelButtons = new JPanel(new GridBagLayout()); + ViewerUtil.setGBC(c, 0, 0, 1, 1, 1.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.HORIZONTAL, + new Insets(0, 0, 0, 0), 0, 0); + panelButtons.add(new JPanel(), c); + ViewerUtil.setGBC(c, 1, 0, 1, 1, 0.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.NONE, + new Insets(0, 0, 0, 8), 0, 0); + panelButtons.add(bOk, c); + ViewerUtil.setGBC(c, 2, 0, 1, 1, 0.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.NONE, + new Insets(0, 0, 0, 0), 0, 0); + panelButtons.add(bCancel, c); + + // putting all together + final JPanel panelMain = new JPanel(new GridBagLayout()); + ViewerUtil.setGBC(c, 0, 0, 1, 1, 1.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.HORIZONTAL, + new Insets(8, 8, 0, 8), 0, 0); + panelMain.add(panelTop, c); + ViewerUtil.setGBC(c, 0, 1, 1, 1, 1.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.HORIZONTAL, + new Insets(16, 8, 0, 8), 0, 0); + panelMain.add(panelSubOverlay, c); + ViewerUtil.setGBC(c, 0, 3, 1, 1, 1.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.HORIZONTAL, + new Insets(16, 8, 0, 8), 0, 0); + panelMain.add(panelHelp, c); + ViewerUtil.setGBC(c, 0, 4, 1, 1, 1.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.HORIZONTAL, + new Insets(8, 8, 8, 8), 0, 0); + panelMain.add(panelButtons, c); + + getContentPane().setLayout(new BorderLayout()); + getContentPane().add(panelMain, BorderLayout.CENTER); + + } + + private void initPvrz() { + // initializing relevant WED information + final WedResource wed = TisConvert.loadWedForTis(tis.entry, true); + if (wed != null) { + final Overlay ovl = (Overlay) wed.getAttribute(Overlay.WED_OVERLAY + " 0"); + if (ovl != null) { + defaultTisWidth = ((IsNumeric) ovl.getAttribute(Overlay.WED_OVERLAY_WIDTH)).getValue(); + defaultTisHeight = ((IsNumeric) ovl.getAttribute(Overlay.WED_OVERLAY_HEIGHT)).getValue(); + final StructEntry se = ovl.getAttribute(Overlay.WED_OVERLAY_MOVEMENT_TYPE); + if (se != null) { + overlayMovementType = ((IsNumeric) se).getValue(); + } + } + } + + // border size + final String helpBorderSize = "Number of pixels to consider from surrounding tiles on PVRZ textures to prevent " + + "visual artifacts. Recommended: " + TisConvert.Config.DEFAULT_BORDER_SIZE + " pixels."; + lBorderSizeLabel = new JLabel("Border Size:"); + lBorderSizeLabel.addMouseMotionListener(listeners.mouseMotion); + helpMap.put(lBorderSizeLabel, helpBorderSize); + + sBorderSize = new JSlider(0, TisConvert.Config.TILE_SIZE, TisConvert.Config.DEFAULT_BORDER_SIZE); + sBorderSize.addMouseMotionListener(listeners.mouseMotion); + sBorderSize.addChangeListener(listeners.changeBorderSize); + helpMap.put(sBorderSize, helpBorderSize); + + lBorderSize = new JLabel(String.format(BORDER_SIZE_LABEL_FMT, sBorderSize.getValue())); + lBorderSize.addMouseMotionListener(listeners.mouseMotion); + helpMap.put(lBorderSize, helpBorderSize); + + // segment size + final String helpSegmentSize = "Defines the max. size of contiguous blocks of secondary tiles that are placed on " + + "PVRZ textures. Smaller sizes may reduce the number of generated PVRZ textures but increase fragmentation. " + + "Recommended: 256 or 512 pixels."; + lSegmentSizeLabel = new JLabel("Segment Size:"); + lSegmentSizeLabel.addMouseMotionListener(listeners.mouseMotion); + helpMap.put(lSegmentSizeLabel, helpSegmentSize); + + sSegmentSize = new JSlider(6, 10, 9); // 64 (1 << 6) to 1024 (1 << 10) + sSegmentSize.addMouseMotionListener(listeners.mouseMotion); + sSegmentSize.addChangeListener(listeners.changeSegmentSize); + helpMap.put(sSegmentSize, helpSegmentSize); + + lSegmentSize = new JLabel(String.format(SEGMENT_SIZE_LABEL_FMT, getSegmentSize())); + lSegmentSize.addMouseMotionListener(listeners.mouseMotion); + helpMap.put(lSegmentSize, helpSegmentSize); + + // pvrz base index + final String helpPvrzBaseIndex = "Defines the start value for page numbers used by the PVRZ filename scheme " + + "(e.g. A260000.PVRZ for index=0, A260013.PVRZ for index=13.) Adjust only to avoid overlapping PVRZ " + + "filenames from other TIS files. Default value: 0"; + lPvrzBaseIndexLabel = new JLabel("PVRZ Base Index:"); + lPvrzBaseIndexLabel.addMouseMotionListener(listeners.mouseMotion); + helpMap.put(lPvrzBaseIndexLabel, helpPvrzBaseIndex); + + sPvrzBaseIndex = new JSlider(0, 99, 0); + sPvrzBaseIndex.addMouseMotionListener(listeners.mouseMotion); + sPvrzBaseIndex.addChangeListener(listeners.changePvrzBaseIndex); + helpMap.put(sPvrzBaseIndex, helpPvrzBaseIndex); + + lPvrzBaseIndex = new JLabel(String.format(PVRZ_BASE_INDEX_LABEL_FMT, getPvrzBaseIndex())); + lPvrzBaseIndex.addMouseMotionListener(listeners.mouseMotion); + helpMap.put(lPvrzBaseIndex, helpPvrzBaseIndex); + + // remove black tiles + final String helpRemoveBlack = "When this option is selected then solid black tiles will be replaced by a global " + + "default which can reduce the number of generated PVRZ textures."; + cbRemoveBlack = new JCheckBox("Optimize black tiles", true); + cbRemoveBlack.addMouseMotionListener(listeners.mouseMotion); + helpMap.put(cbRemoveBlack, helpRemoveBlack); + + // multithreaded + final String helpMultithreaded = "Specifies whether to speed up PVRZ texture encoding by using multiple threads " + + "of execution."; + final int numCores = Runtime.getRuntime().availableProcessors(); + cbMultithreaded = new JCheckBox("Multithreaded", numCores > 2); + cbMultithreaded.setEnabled(numCores > 1); + cbMultithreaded.addMouseMotionListener(listeners.mouseMotion); + helpMap.put(cbMultithreaded, helpMultithreaded); + + // radio buttons + final ButtonGroup buttonGroup = new ButtonGroup(); + final String helpRadioAuto = "Automatically configures tileset parameters based on WED information. " + + "This mode is only available if an associated WED resource exists."; + rbAuto = new JRadioButton("Automatic (recommended)"); + rbAuto.setMnemonic(KeyEvent.VK_A); + rbAuto.setEnabled(wedEntry != null); + rbAuto.addMouseMotionListener(listeners.mouseMotion); + rbAuto.addItemListener(listeners.itemAuto); + helpMap.put(rbAuto, helpRadioAuto); + buttonGroup.add(rbAuto); + + final String helpRadioManual = "Manual customization of tileset parameters. This is the only choice if no " + + "associated WED resource could be determined. This mode does not prevent visual artifacts around secondary " + + "tiles."; + rbManual = new JRadioButton("Manual"); + rbManual.setMnemonic(KeyEvent.VK_M); + rbManual.addMouseMotionListener(listeners.mouseMotion); + rbManual.addItemListener(listeners.itemManual); + helpMap.put(rbManual, helpRadioManual); + buttonGroup.add(rbManual); + + // tiles per row + final String helpTilesPerRow = "Defines the width of the tileset, in tiles. An incorrect width will create a " + + "distorted tileset layout which increases the likelihood of visual artifacts with PVRZ-based tilesets."; + lTilesPerRowLabel = new JLabel("Tiles per Row:"); + lTilesPerRowLabel.addMouseMotionListener(listeners.mouseMotion); + helpMap.put(lTilesPerRowLabel, helpTilesPerRow); + + sTilesPerRow = new JSlider(1, tis.decoder.getTileCount()); + sTilesPerRow.setValue(Math.max(1, Math.min(tis.decoder.getTileCount(), tis.slCols.getValue()))); + sTilesPerRow.addMouseMotionListener(listeners.mouseMotion); + sTilesPerRow.addChangeListener(listeners.changeTilesPerRow); + helpMap.put(sTilesPerRow, helpTilesPerRow); + + lTilesPerRow = new JLabel(String.format(TILES_PER_ROW_LABEL_FMT, sTilesPerRow.getValue())); + lTilesPerRow.setPreferredSize(new Dimension(bTisFile.getPreferredSize().width, lTilesPerRow.getPreferredSize().height)); + lTilesPerRow.addMouseMotionListener(listeners.mouseMotion); + helpMap.put(lTilesPerRow, helpTilesPerRow); + + // tileset height + final String helpNumRows = "Defines the height of the tileset, in tiles. All tiles exceeding the amount of " + + "'Tiles Per Row' x 'Tileset Height' are treated as secondary tiles."; + lNumRowsLabel = new JLabel("Tileset Height:"); + lNumRowsLabel.addMouseMotionListener(listeners.mouseMotion); + helpMap.put(lNumRowsLabel, helpNumRows); + + sNumRows = new JSlider(1, tis.decoder.getTileCount() / sTilesPerRow.getValue()); + sNumRows.setValue((defaultTisHeight > 0) ? defaultTisHeight : sNumRows.getMaximum()); + sNumRows.addMouseMotionListener(listeners.mouseMotion); + sNumRows.addChangeListener(listeners.changeNumRows); + helpMap.put(sNumRows, helpNumRows); + + lNumRows = new JLabel(String.format(ROW_COUNT_LABEL_FMT, sNumRows.getValue())); + lNumRows.setPreferredSize(new Dimension(bTisFile.getPreferredSize().width, lTilesPerRow.getPreferredSize().height)); + lNumRows.addMouseMotionListener(listeners.mouseMotion); + helpMap.put(lNumRows, helpNumRows); + + // tileset preview + final String helpPreviewImage = "A visual representation of the tileset layout to help setting the right width " + + "and height parameters."; + lPreviewImageLabel = new JLabel("Tileset Preview:"); + lPreviewImageLabel.addMouseMotionListener(listeners.mouseMotion); + helpMap.put(lPreviewImageLabel, helpPreviewImage); + + try { + tisPreview = new TisPreview(tis.decoder); + } catch (Exception e) { + e.printStackTrace(); + } + + final int previewWidth = sTilesPerRow.getValue(); + final int previewHeight = sNumRows.getValue(); + lPreviewImage = new JLabel(new ImageIcon(tisPreview.get(previewWidth, previewHeight))); + lPreviewImage.setHorizontalAlignment(SwingConstants.LEFT); + lPreviewImage.setVerticalAlignment(SwingConstants.TOP); + lPreviewImage.addMouseMotionListener(listeners.mouseMotion); + helpMap.put(lPreviewImage, helpPreviewImage); + + spPreviewImage = new JScrollPane(lPreviewImage, JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, + JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED); + spPreviewImage.setBorder(BorderFactory.createLineBorder(Color.BLACK)); + spPreviewImage.getHorizontalScrollBar().setUnitIncrement(16); + spPreviewImage.getVerticalScrollBar().setUnitIncrement(16); + // setting default preview dimension + if (defaultTisWidth > 0 && defaultTisHeight > 0) { + final int width = defaultTisWidth * tisPreview.getTileSize(); + final int height = ((tis.decoder.getTileCount() + defaultTisWidth - 1) / defaultTisWidth) * tisPreview.getTileSize(); + spPreviewImage.setPreferredSize(new Dimension(width + 2, height + 2)); + } else { + final int tisWidth = Math.min(80, previewWidth) * tisPreview.getTileSize(); + final int height = (tis.decoder.getTileCount() + previewWidth - 1) / previewWidth; + final int tisHeight = Math.min(60, height) * tisPreview.getTileSize(); + spPreviewImage.setPreferredSize(new Dimension(tisWidth + 2, tisHeight + 2)); } - boolean isNight = (Character.toUpperCase(name.charAt(name.length() - 1)) == 'N'); - if (name.length() > 7) { - int numDelete = name.length() - 7; - int ofsDelete = name.length() - numDelete - (isNight ? 1 : 0); - name = name.substring(ofsDelete, numDelete); - return path.resolve(name); - } else if (name.length() < 2) { - String fmt, newName = null; - int maxNum; - switch (name.length()) { - case 0: - fmt = name + "%s02d"; - maxNum = 99; + bPreviewReset = new JButton("Reset"); + bPreviewReset.setEnabled(defaultTisHeight > 0); + bPreviewReset.setToolTipText("Click to reset tileset dimensions"); + bPreviewReset.addMouseMotionListener(listeners.mouseMotion); + bPreviewReset.addActionListener(listeners.actionPreviewReset); + helpMap.put(bPreviewReset, helpPreviewImage); + + lPreviewImageZoomLabel = new JLabel("Zoom:"); + lPreviewImageZoomLabel.addMouseMotionListener(listeners.mouseMotion); + helpMap.put(lPreviewImageZoomLabel, helpPreviewImage); + + final Vector> previewImageZoomValues = new Vector<>(); + for (final int zoom : new int[] {1, 2, 3, 4, 6, 8, 12, 16}) { + previewImageZoomValues.add(new DataString(String.format("%dx", zoom), zoom)); + } + cbPreviewImageZoom = new JComboBox>(previewImageZoomValues); + cbPreviewImageZoom.setSelectedIndex(getPreviewZoomIndex(tisPreview.getTileSize(), 2)); + cbPreviewImageZoom.addMouseMotionListener(listeners.mouseMotion); + cbPreviewImageZoom.addItemListener(listeners.itemPreviewImageZoom); + helpMap.put(cbPreviewImageZoom, helpPreviewImage); + + // laying out components on dialog + final GridBagConstraints c = new GridBagConstraints(); + + // top panel (tis file, border size) + final JPanel panelTop = new JPanel(new GridBagLayout()); + ViewerUtil.setGBC(c, 0, 0, 1, 1, 0.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.NONE, + new Insets(0, 0, 0, 8), 0, 0); + panelTop.add(lTisFileLabel, c); + ViewerUtil.setGBC(c, 1, 0, 1, 1, 1.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.HORIZONTAL, + new Insets(0, 0, 0, 8), 0, 0); + panelTop.add(tfTisFile, c); + ViewerUtil.setGBC(c, 2, 0, 1, 1, 0.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.NONE, + new Insets(0, 0, 0, 0), 0, 0); + panelTop.add(bTisFile, c); + + ViewerUtil.setGBC(c, 0, 1, 1, 1, 0.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.NONE, + new Insets(8, 0, 0, 8), 0, 0); + panelTop.add(lBorderSizeLabel, c); + ViewerUtil.setGBC(c, 1, 1, 1, 1, 1.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.HORIZONTAL, + new Insets(8, 0, 0, 8), 0, 0); + panelTop.add(sBorderSize, c); + ViewerUtil.setGBC(c, 2, 1, 1, 1, 0.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.NONE, + new Insets(8, 0, 0, 0), 0, 0); + panelTop.add(lBorderSize, c); + + ViewerUtil.setGBC(c, 0, 2, 1, 1, 0.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.NONE, + new Insets(8, 0, 0, 8), 0, 0); + panelTop.add(lSegmentSizeLabel, c); + ViewerUtil.setGBC(c, 1, 2, 1, 1, 1.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.HORIZONTAL, + new Insets(8, 0, 0, 8), 0, 0); + panelTop.add(sSegmentSize, c); + ViewerUtil.setGBC(c, 2, 2, 1, 1, 0.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.NONE, + new Insets(8, 0, 0, 0), 0, 0); + panelTop.add(lSegmentSize, c); + + ViewerUtil.setGBC(c, 0, 3, 1, 1, 0.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.NONE, + new Insets(8, 0, 0, 8), 0, 0); + panelTop.add(lPvrzBaseIndexLabel, c); + ViewerUtil.setGBC(c, 1, 3, 1, 1, 1.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.HORIZONTAL, + new Insets(8, 0, 0, 8), 0, 0); + panelTop.add(sPvrzBaseIndex, c); + ViewerUtil.setGBC(c, 2, 3, 1, 1, 0.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.NONE, + new Insets(8, 0, 0, 0), 0, 0); + panelTop.add(lPvrzBaseIndex, c); + + // sub-panel for checkbox options + final JPanel panelSubOptions = new JPanel(new GridBagLayout()); + ViewerUtil.setGBC(c, 0, 0, 1, 1, 0.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.NONE, + new Insets(0, 0, 0, 8), 0, 0); + panelSubOptions.add(cbRemoveBlack, c); + ViewerUtil.setGBC(c, 1, 0, 1, 1, 0.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.NONE, + new Insets(0, 0, 0, 8), 0, 0); + panelSubOptions.add(cbMultithreaded, c); + + ViewerUtil.setGBC(c, 0, 4, 1, 1, 0.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.NONE, + new Insets(8, 0, 0, 0), 0, 0); + panelTop.add(new JPanel(), c); + ViewerUtil.setGBC(c, 1, 4, 2, 1, 0.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.NONE, + new Insets(8, 0, 0, 0), 0, 0); + panelTop.add(panelSubOptions, c); + + // sub-panel for Auto mode (overlay conversion) + panelSubAuto = new JPanel(new GridBagLayout()); + ViewerUtil.setGBC(c, 0, 0, 1, 1, 0.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.NONE, + new Insets(0, 0, 0, 8), 0, 0); + panelSubAuto.add(lOverlayModeLabel, c); + ViewerUtil.setGBC(c, 1, 0, 1, 1, 0.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.NONE, + new Insets(0, 0, 0, 0), 0, 0); + panelSubAuto.add(cbOverlayMode, c); + ViewerUtil.setGBC(c, 2, 0, 1, 1, 1.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.HORIZONTAL, + new Insets(0, 0, 0, 0), 0, 0); + panelSubAuto.add(new JPanel(), c); + + // Auto mode panel + final JPanel panelAutoMode = new JPanel(new GridBagLayout()); + ViewerUtil.setGBC(c, 0, 0, 1, 1, 0.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.NONE, + new Insets(0, 0, 0, 0), 0, 0); + panelAutoMode.add(rbAuto, c); + ViewerUtil.setGBC(c, 0, 1, 1, 1, 1.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.HORIZONTAL, + new Insets(12, 24, 0, 0), 0, 0); + panelAutoMode.add(panelSubAuto, c); + + // sub-panel for Manual mode (cols, rows, preview) + panelSubManual = new JPanel(new GridBagLayout()); + ViewerUtil.setGBC(c, 0, 0, 1, 1, 0.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.NONE, + new Insets(0, 0, 0, 8), 0, 0); + panelSubManual.add(lTilesPerRowLabel, c); + ViewerUtil.setGBC(c, 1, 0, 1, 1, 1.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.HORIZONTAL, + new Insets(0, 0, 0, 8), 0, 0); + panelSubManual.add(sTilesPerRow, c); + ViewerUtil.setGBC(c, 2, 0, 1, 1, 0.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.NONE, + new Insets(0, 0, 0, 0), 0, 0); + panelSubManual.add(lTilesPerRow, c); + + ViewerUtil.setGBC(c, 0, 1, 1, 1, 0.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.NONE, + new Insets(8, 0, 0, 8), 0, 0); + panelSubManual.add(lNumRowsLabel, c); + ViewerUtil.setGBC(c, 1, 1, 1, 1, 1.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.HORIZONTAL, + new Insets(8, 0, 0, 8), 0, 0); + panelSubManual.add(sNumRows, c); + ViewerUtil.setGBC(c, 2, 1, 1, 1, 0.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.NONE, + new Insets(8, 0, 0, 0), 0, 0); + panelSubManual.add(lNumRows, c); + + ViewerUtil.setGBC(c, 0, 2, 1, 1, 0.0, 0.0, GridBagConstraints.FIRST_LINE_START, GridBagConstraints.NONE, + new Insets(8, 0, 0, 8), 0, 0); + panelSubManual.add(lPreviewImageLabel, c); + ViewerUtil.setGBC(c, 0, 3, 1, 1, 0.0, 0.0, GridBagConstraints.LAST_LINE_END, GridBagConstraints.NONE, + new Insets(8, 0, 0, 8), 0, 0); + panelSubManual.add(bPreviewReset, c); + ViewerUtil.setGBC(c, 1, 2, 1, 2, 1.0, 1.0, GridBagConstraints.FIRST_LINE_START, GridBagConstraints.BOTH, + new Insets(8, 8, 0, 8), 0, 0); + panelSubManual.add(spPreviewImage, c); + ViewerUtil.setGBC(c, 2, 2, 1, 1, 0.0, 0.0, GridBagConstraints.FIRST_LINE_START, GridBagConstraints.NONE, + new Insets(8, 0, 0, 0), 0, 0); + panelSubManual.add(lPreviewImageZoomLabel, c); + ViewerUtil.setGBC(c, 2, 3, 1, 1, 0.0, 0.0, GridBagConstraints.FIRST_LINE_START, GridBagConstraints.NONE, + new Insets(8, 0, 0, 0), 0, 0); + panelSubManual.add(cbPreviewImageZoom, c); + + // Manual mode panel + final JPanel panelManualMode = new JPanel(new GridBagLayout()); + ViewerUtil.setGBC(c, 0, 0, 1, 1, 0.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.NONE, + new Insets(0, 0, 0, 0), 0, 0); + panelManualMode.add(rbManual, c); + ViewerUtil.setGBC(c, 0, 1, 1, 1, 1.0, 1.0, GridBagConstraints.LINE_START, GridBagConstraints.BOTH, + new Insets(12, 24, 0, 0), 0, 0); + panelManualMode.add(panelSubManual, c); + + // help panel + final JPanel panelHelp = new JPanel(new GridBagLayout()); + ViewerUtil.setGBC(c, 0, 0, 1, 1, 0.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.NONE, + new Insets(0, 0, 0, 0), 0, 0); + panelHelp.add(lHelpLabel, c); + ViewerUtil.setGBC(c, 0, 1, 1, 1, 1.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.HORIZONTAL, + new Insets(8, 0, 0, 0), 0, 0); + panelHelp.add(spHelp, c); + + // button panel + final JPanel panelButtons = new JPanel(new GridBagLayout()); + ViewerUtil.setGBC(c, 0, 0, 1, 1, 1.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.HORIZONTAL, + new Insets(0, 0, 0, 0), 0, 0); + panelButtons.add(new JPanel(), c); + ViewerUtil.setGBC(c, 1, 0, 1, 1, 0.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.NONE, + new Insets(0, 0, 0, 8), 0, 0); + panelButtons.add(bOk, c); + ViewerUtil.setGBC(c, 2, 0, 1, 1, 0.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.NONE, + new Insets(0, 0, 0, 0), 0, 0); + panelButtons.add(bCancel, c); + + // putting all together + final JPanel panelMain = new JPanel(new GridBagLayout()); + ViewerUtil.setGBC(c, 0, 0, 1, 1, 1.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.HORIZONTAL, + new Insets(8, 8, 0, 8), 0, 0); + panelMain.add(panelTop, c); + ViewerUtil.setGBC(c, 0, 1, 1, 1, 1.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.HORIZONTAL, + new Insets(16, 8, 0, 8), 0, 0); + panelMain.add(panelAutoMode, c); + ViewerUtil.setGBC(c, 0, 2, 1, 1, 1.0, 1.0, GridBagConstraints.LINE_START, GridBagConstraints.BOTH, + new Insets(16, 8, 0, 8), 0, 0); + panelMain.add(panelManualMode, c); + ViewerUtil.setGBC(c, 0, 3, 1, 1, 1.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.HORIZONTAL, + new Insets(16, 8, 0, 8), 0, 0); + panelMain.add(panelHelp, c); + ViewerUtil.setGBC(c, 0, 4, 1, 1, 1.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.HORIZONTAL, + new Insets(8, 8, 8, 8), 0, 0); + panelMain.add(panelButtons, c); + + getContentPane().setLayout(new BorderLayout()); + getContentPane().add(panelMain, BorderLayout.CENTER); + + // preselecting mode + if (wedEntry != null) { + rbAuto.setSelected(true); + } else { + rbManual.setSelected(true); + } + setPanelEnabled(panelSubAuto, rbAuto.isSelected()); + setPanelEnabled(panelSubManual, !rbAuto.isSelected()); + } + + private JComboBox> initOverlayModeComboBox(boolean preselect) { + final boolean isTisV1 = tis.decoder instanceof TisV1Decoder; + final boolean isTisV2 = tis.decoder instanceof TisV2Decoder; + final boolean isBG1 = Profile.getEngine() == Profile.Engine.BG1; + final boolean isBG2 = Profile.getEngine() == Profile.Engine.BG2; + final boolean isEE = Profile.isEnhancedEdition(); + final boolean isBGEE = (Profile.getGame() == Profile.Game.BG1EE) || (Profile.getGame() == Profile.Game.BG1SoD); + final boolean hasWed = (wedEntry != null); + + // initializing overlay mode combobox + final Vector> overlayModes = new Vector<>(); + int overlayModeIndex = 0; + for (final TisConvert.OverlayConversion mode : TisConvert.OverlayConversion.values()) { + boolean add = false; + boolean select = false; + switch (mode) { + case BG1_TO_BGEE: + add = hasWed && isTisV1; + select = isBG1 || + (overlayMovementType == 0 && isBGEE) || + (overlayMovementType == 2 && isEE && !isBGEE); break; - default: - fmt = name + "%s01d"; - maxNum = 9; + case BG1_TO_BG2EE: + add = hasWed && isTisV1; break; - } - for (int i = 0; i < maxNum; i++) { - String s = String.format(fmt, i) + (isNight ? "N" : "") + ext; - if (!ResourceFactory.resourceExists(s)) { - newName = s; + case BG2_TO_BGEE: + add = hasWed && isTisV1; break; - } + case BG2_TO_BG2EE: + add = hasWed && isTisV1; + select = isBG2 || + (overlayMovementType == 0 && isEE && !isBGEE) || + (overlayMovementType == 2 && isBGEE); + break; + case BGEE_TO_BG1: + add = hasWed && isTisV2; + select = isBG1 || + (overlayMovementType == 0 && isBGEE) || + (overlayMovementType == 2 && isEE && !isBGEE); + break; + case BGEE_TO_BG2: + add = hasWed && isTisV2; + break; + case BG2EE_TO_BG1: + add = hasWed && isTisV2; + break; + case BG2EE_TO_BG2: + add = hasWed && isTisV2; + select = isBG2 || + (overlayMovementType == 0 && isEE && !isBGEE) || + (overlayMovementType == 2 && isBGEE); + break; + default: + add = true; } - if (newName != null) { - return path.resolve(newName); + + if (mode.isImplemented() && add) { + final String modeText = mode.toString(); + final DataString entry = + new DataString(modeText, mode, DataString.FMT_STRING_ONLY); + overlayModes.add(entry); + if (select) { + overlayModeIndex = overlayModes.size() - 1; + } } } + + final JComboBox> retVal = new JComboBox<>(overlayModes); + if (preselect) { + retVal.setSelectedIndex(overlayModeIndex); + } + return retVal; } - return fileName; - } - /** - * Attempts to find and load the WED resource associated with the specified TIS resource. - * - * @param tisEntry The TIS resource entry. - * @return {@code WedResource} instance if successful, {@code null} otherwise. - */ - public static WedResource loadWedForTis(ResourceEntry tisEntry) { - WedResource wed = null; - - if (tisEntry != null) { - String tisBase = tisEntry.getResourceRef(); - ResourceEntry wedEntry = null; - while (tisBase.length() >= 6) { - String wedName = tisBase + ".WED"; - wedEntry = ResourceFactory.getResourceEntry(wedName); - if (wedEntry != null) { - break; - } else { - tisBase = tisBase.substring(0, tisBase.length() - 1); + /** Enables or disables all child component of the given panel. */ + private void setPanelEnabled(JComponent panel, boolean enable) { + if (panel != null) { + for (final Component c : panel.getComponents()) { + c.setEnabled(enable); } } + } - if (wedEntry != null) { - try { - wed = new WedResource(wedEntry); - } catch (Exception e) { + /** + * Returns the combobox item index that defines the specified {@code zoom}. Returns {@code defIndex} if no matching + * entry could be found. + */ + private int getPreviewZoomIndex(int zoom, int defIndex) { + for (int idx = 0, size = cbPreviewImageZoom.getModel().getSize(); idx < size; idx++) { + final int zoomValue = cbPreviewImageZoom.getModel().getElementAt(idx).getData(); + if (zoomValue == zoom) { + return idx; } } + return defIndex; } - return wed; - } + /** + * Checks current dialog settings. + * + * @param interactive Indicates whether a message dialog should notify about failed checks. + * @return {@code true} if all checks passed successfully, {@code false} otherwise. + */ + private boolean validateSettings(boolean interactive) { + // is TIS path empty? + if (tfTisFile.getText().trim().isEmpty()) { + if (interactive) { + JOptionPane.showMessageDialog(this, "No TIS output file specified.", "Error", JOptionPane.ERROR_MESSAGE); + tfTisFile.requestFocus(); + } + return false; + } - /** - * Attempts to calculate the TIS width from an associated WED file. - * - * @param entry The TIS resource entry. - * @param tileCount An optional tile count that will be used to "guess" the correct number of tiles per row if no - * associated WED resource has been found. - * @return The number of tiles per row for the specified TIS resource. - */ - public static int calcTileWidth(ResourceEntry entry, int tileCount) { - // Try to fetch the correct width from an associated WED if available - if (entry != null) { try { - String tisNameBase = entry.getResourceRef(); - ResourceEntry wedEntry = null; - while (tisNameBase.length() >= 6) { - String wedFileName = tisNameBase + ".WED"; - wedEntry = ResourceFactory.getResourceEntry(wedFileName); - if (wedEntry != null) { - break; - } else { - tisNameBase = tisNameBase.substring(0, tisNameBase.length() - 1); + final Path tisPath = Paths.get(tfTisFile.getText().trim()); + + // does TIS filename conform to expected naming scheme? + if (tis.getDecoder() instanceof TisV1Decoder && !TisConvert.isTisFileNameValid(tisPath)) { + if (interactive) { + JOptionPane.showMessageDialog(this, "PVRZ-based TIS filenames have to be 2 up to 7 characters long.", + "Error", JOptionPane.ERROR_MESSAGE); } + return false; } - if (wedEntry != null) { - ByteBuffer wed = wedEntry.getResourceBuffer(); - if (wed != null) { - String sig = StreamUtils.readString(wed, 0, 8); - if (sig.equals("WED V1.3")) { - final int sizeOvl = 0x18; - int numOvl = wed.getInt(8); - int ofsOvl = wed.getInt(16); - for (int i = 0; i < numOvl; i++) { - int ofs = ofsOvl + i * sizeOvl; - String tisName = StreamUtils.readString(wed, ofs + 4, 8); - if (tisName.equalsIgnoreCase(tisNameBase)) { - int width = wed.getShort(ofs); - if (width > 0) { - return width; - } - } - } + + // does TIS file already exist? + if (Files.exists(tisPath)) { + int result = 1; + if (interactive) { + final String[] options = { "Overwrite", "Cancel" }; + result = JOptionPane.showOptionDialog(this, tisPath + " exists. Overwrite?", "Question", + JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE, null, options, options[0]); + if (result != 0) { + tfTisFile.requestFocus(); } } + if (result != 0) { + return false; + } } - } catch (Exception e) { + } catch (InvalidPathException e) { + // is TIS path valid? + if (interactive) { + JOptionPane.showMessageDialog(this, "Invalid TIS output file specified.", "Error", JOptionPane.ERROR_MESSAGE); + tfTisFile.requestFocus(); + } + return false; } - } - // If WED is not available: approximate the most commonly used aspect ratio found in TIS files - // Disadvantage: does not take extra tiles into account - return (tileCount < 9) ? tileCount : (int) (Math.sqrt(tileCount) * 1.18); - } - // Calculates a Dimension structure with the correct number of columns and rows from the specified arguments - private static Dimension calcGridSize(int imageCount, int colSize) { - if (imageCount >= 0 && colSize > 0) { - int rowSize = imageCount / colSize; - if (imageCount % colSize > 0) { - rowSize++; + // PVRZ base index overlaps with existing PVRZ files? + if (tis.getDecoder() instanceof TisV1Decoder) { + final int pvrzBaseIndex = getPvrzBaseIndex(); + final int newPvrzBaseIndex = TisConvert.calcPvrzBaseIndex(getTisPath()); + if (newPvrzBaseIndex > pvrzBaseIndex) { + if (interactive) { + final String[] options = { "Update", "Keep", "Cancel" }; + final int result = JOptionPane.showOptionDialog(this, + "PVRZ base index overlaps with existing files.\n\nPlease choose:\n" + + "Update: Update to next available free index (" + newPvrzBaseIndex + ") and continue.\n" + + "Keep: Keep selected index (" + pvrzBaseIndex + ") and overwrite existing files.\n" + + "Cancel: Return to the dialog and adjust manually.\n ", + "Confirm", + JOptionPane.YES_NO_CANCEL_OPTION, + JOptionPane.WARNING_MESSAGE, + null, options, options[0]); + switch (result) { + case 0: // Adjust + sPvrzBaseIndex.setValue(newPvrzBaseIndex); + break; + case 1: // Keep + break; + default: // Cancel + sPvrzBaseIndex.requestFocus(); + return false; + } + } else { + return false; + } + } } - return new Dimension(colSize, Math.max(1, rowSize)); - } - return null; - } - /** Returns whether the specified PVRZ index can be found in the current TIS resource. */ - public boolean containsPvrzReference(int index) { - boolean retVal = false; - if (index >= 0 && index <= 99) { - if (decoder instanceof TisV2Decoder) { - TisV2Decoder tisv2 = (TisV2Decoder) decoder; - for (int i = 0, count = tisv2.getTileCount(); i < count && !retVal; i++) { - retVal = (tisv2.getPvrzPage(i) == index); + // Manual mode: number of secondary tiles must be less than or equal to number of primary tiles + if (tis.getDecoder() instanceof TisV1Decoder && isManualMode()) { + final int numPriTiles = getRowCount() * getTilesPerRow(); + final int numSecTiles = tis.getDecoder().getTileCount() - numPriTiles; + if (numSecTiles > numPriTiles) { + if (interactive) { + JOptionPane.showMessageDialog(this, "Number of secondary tiles must not be greater than number\n" + + "of primary tiles. Please adjust tileset height accordingly.", "Error", JOptionPane.ERROR_MESSAGE); + sNumRows.requestFocus(); + } + return false; } } - } - return retVal; - } - - // -------------------------- INNER CLASSES -------------------------- - private final class PopupListener extends MouseAdapter { - @Override - public void mousePressed(MouseEvent e) { - maybeShowPopup(e); + return true; } - @Override - public void mouseReleased(MouseEvent e) { - maybeShowPopup(e); - } + /** + * Returns a valid TIS file path, based on the given input file name. + * + * @param fileName Filename string to validate. + * @param strict Specifies whether the given filename must be preserved. + * @return Validated, and possibly corrected, filename, as {@link Path} object. + */ + private Path validateTisFileName(String fileName, boolean strict) { + Path retVal = null; + try { + retVal = Paths.get(fileName); + if (retVal.getParent() == null || !Files.exists(retVal.getParent())) { + retVal = null; + } + } catch (NullPointerException | IllegalArgumentException e) { + } - private void maybeShowPopup(MouseEvent e) { - if (e.isPopupTrigger()) { - int index = tileGrid.getTileIndexAt(new Point(e.getX(), e.getY())); - if (index >= 0 && index < decoder.getTileCount()) { - lastTileInfoIndex = index; + if (strict) { + return retVal; + } - miTileInfoShow.setText(String.format(FMT_TILEINFO_SHOW, lastTileInfoIndex)); - miTileInfoShow.setVisible(decoder.getType() == TisDecoder.Type.PVRZ); + if (retVal == null) { + try { + retVal = Profile.getGameRoot().resolve(tis.getResourceEntry().getResourceName()); + if (retVal.getParent() == null || !Files.exists(retVal.getParent())) { + retVal = null; + } + } catch (IllegalArgumentException e) { + } + } - miTileInfoPvrz.setText(String.format(FMT_TILEINFO_PVRZ, lastTileInfoIndex)); - miTileInfoPvrz.setVisible(decoder.getType() == TisDecoder.Type.PVRZ); + if (retVal == null) { + try { + retVal = Profile.getGameRoot().resolve("OUTPUT.TIS"); + } catch (IllegalArgumentException e) { + } + } - miTileInfoWed.setText(String.format(FMT_TILEINFO_WED, lastTileInfoIndex)); - miTileInfoWed.setVisible(wedTileMap != null && wedTileMap.containsKey(lastTileInfoIndex)); + return retVal; + } - if (miTileInfoShow.isVisible() || miTileInfoPvrz.isVisible() || miTileInfoWed.isVisible()) { - menuTileInfo.show(e.getComponent(), e.getX(), e.getY()); - } - } + /** Returns the {@link Path} of the output TIS file. */ + private Path getTisPath() { + try { + return Paths.get(tfTisFile.getText().trim()); + } catch (InvalidPathException e) { + e.printStackTrace(); } + return null; } - } - // Tracks regions of tiles used for the tile -> pvrz packing algorithm - private static class TileRect { - private Dimension bounds; - private int[] indices; + /** Returns the border size, in pixels. */ + private int getBorderSize() { + return (sBorderSize != null) ? sBorderSize.getValue() : 0; + } - /** Creates an empty TileRect structure. */ - TileRect(int width, int height) { - width = Math.max(1, width); - height = Math.max(1, height); - bounds = new Dimension(width, height); - indices = new int[width * height]; - Arrays.fill(indices, -1); + /** Returns the segment size, in pixels. */ + private int getSegmentSize() { + return (sSegmentSize != null) ? 1 << sSegmentSize.getValue() : 0; } - /** Automatically fills the TileRect structure with valid tile indices. */ - TileRect(int left, int top, int width, int height, int rowLength, int numTiles, boolean[] markedTiles) { - left = Math.max(0, left); - top = Math.max(0, top); - width = Math.max(1, width); - height = Math.max(1, height); - rowLength = Math.max(width, rowLength); - bounds = new Dimension(width, height); - indices = new int[width * height]; - for (int by = 0; by < height; by++) { - int idx = by * width; - int ofs = (top + by) * rowLength; - for (int bx = 0; bx < width; bx++) { - int tileIdx = ofs + left + bx; - if (tileIdx < numTiles) { - indices[idx + bx] = tileIdx; - if (tileIdx < markedTiles.length) { - markedTiles[tileIdx] = true; - } - } else { - indices[idx + bx] = -1; - } - } - } + /** Returns the PVRZ base index. */ + private int getPvrzBaseIndex() { + return (sPvrzBaseIndex != null) ? sPvrzBaseIndex.getValue() : 0; + } + + /** Returns whether black tiles should be detected and optimized. */ + private boolean isDetectBlack() { + return (cbRemoveBlack != null) ? cbRemoveBlack.isSelected() : false; + } + + /** Returns whether multithreading is used to encode DXT1 pixel data. */ + private boolean isMultithreaded() { + return (cbMultithreaded != null) ? cbMultithreaded.isSelected() : false; } /** - * Sets the specified tile index in the TileRect structure. x and y specify a position within the TileRect - * structure. tileIndex is the absolute tile index. + * Returns whether automatic configuration mode is enabled. Always returns {@code true} for PVRZ->Palette + * conversion. */ - public boolean setMarked(int x, int y, int tileIndex) { - tileIndex = Math.max(-1, tileIndex); - if (x >= 0 && x < bounds.width && y >= 0 && y < bounds.height) { - int index = y * bounds.width + x; - if ((tileIndex != -1 && indices[index] == -1) || (tileIndex == -1 && indices[index] != -1)) { - indices[index] = tileIndex; - return true; - } + private boolean isAutoMode() { + return (rbAuto != null) ? rbAuto.isSelected() : true; + } + + /** + * Returns whether manual configuration mode is enabled. Always returns {@code true} for PVRZ->Palette + * conversion. + */ + private boolean isManualMode() { + return (rbManual != null) ? rbManual.isSelected() : true; + } + + /** Returns the selected overlay conversion mode. */ + private TisConvert.OverlayConversion getOverlayConversionMode() { + if (isAutoMode()) { + return cbOverlayMode.getModel().getElementAt(cbOverlayMode.getSelectedIndex()).getData(); + } else { + return TisConvert.OverlayConversion.NONE; } - return false; } - public int getX(int index) { - return (index >= 0 && index < bounds.width * bounds.height) ? index % bounds.width : -1; + /** Returns the number of tiles per row. */ + private int getTilesPerRow() { + return (sTilesPerRow != null && rbManual.isSelected()) ? sTilesPerRow.getValue() : tis.slCols.getValue(); + } + + /** Returns the number of tileset rows. */ + private int getRowCount() { + return (sNumRows != null && rbManual.isSelected()) ? sNumRows.getValue() : -1; } - public int getY(int index) { - return (index >= 0 && index < bounds.width * bounds.height) ? index / bounds.width : -1; + /** Returns the list of tile images. Returns {@code null} for palette->PVRZ conversions. */ + private List getTileList() { + return (tis.decoder instanceof TisV2Decoder) ? tis.getTileList() : null; + } + + /** Child class is used to implement event-related functionality. */ + private class Listeners { + /** WindowListener: reset "accepted" flag when dialog is shown */ + private final WindowAdapter windowDialog = new WindowAdapter() { + public void windowOpened(WindowEvent e) { accepted = false; } + }; + + /** MouseMotionListener: updates help text */ + private final MouseMotionAdapter mouseMotion = new MouseMotionAdapter() { + public void mouseMoved(MouseEvent e) { + final String msg = helpMap.getOrDefault(e.getSource(), ""); + taHelp.setText(msg); + taHelp.setCaretPosition(0); + } + }; + + /** ActionListener: JButton bPreviewReset */ + private final ActionListener actionPreviewReset = e -> { + if (bPreviewReset.isEnabled()) { + if (defaultTisWidth > 0 && defaultTisHeight > 0) { + sTilesPerRow.setValue(defaultTisWidth); + sNumRows.setValue(defaultTisHeight); + } + } + }; + + /** ActionListener: JButton bOK */ + private final ActionListener actionOk = e -> { + if (validateSettings(true)) { + accepted = true; + setVisible(false); + } + }; + + /** ActionListener: JButton bCancel */ + private final ActionListener actionCancel = e -> setVisible(false); + + /** ActionListener: JButton bTisFile */ + private final ActionListener actionTisFile = e -> { + final Path outPath = validateTisFileName(tfTisFile.getText(), false); + final FileNameExtensionFilter tisFilter = new FileNameExtensionFilter("TIS files (*.tis)", "tis"); + final JFileChooser chooser = new JFileChooser(outPath.toFile()); + chooser.setSelectedFile(outPath.toFile()); + chooser.setDialogType(JFileChooser.SAVE_DIALOG); + chooser.setDialogTitle("Choose output TIS file"); + chooser.addChoosableFileFilter(tisFilter); + chooser.setFileFilter(tisFilter); + final int retVal = chooser.showSaveDialog(ConvertTisDialog.this); + if (retVal == JFileChooser.APPROVE_OPTION) { + // add extension if needed + final String tisPath = chooser.getSelectedFile().toString().trim(); + final String tisFile = chooser.getSelectedFile().getName().trim(); + final int extIndex = tisFile.indexOf('.'); + final String ext; + if (extIndex < 0) { + ext = ".TIS"; + } else if (extIndex == tisFile.length() - 1) { + ext = "TIS"; + } else { + ext = ""; + } + tfTisFile.setText(tisPath + ext); + } + }; + + /** ChangeListener: JSlider sBorderSize */ + private final ChangeListener changeBorderSize = e -> { + final int size = sBorderSize.getValue(); + lBorderSize.setText(String.format(BORDER_SIZE_LABEL_FMT, size)); + }; + + /** ChangeListener: JSlider sSegmentSize */ + private final ChangeListener changeSegmentSize = e -> { + lSegmentSize.setText(String.format(SEGMENT_SIZE_LABEL_FMT, getSegmentSize())); + }; + + /** ChangeListener: JSlider: sPvrzBaseIndex */ + private final ChangeListener changePvrzBaseIndex = e -> { + lPvrzBaseIndex.setText(String.format(PVRZ_BASE_INDEX_LABEL_FMT, getPvrzBaseIndex())); + }; + + /** ChangeListener: JSlider sTilesPerRow */ + private final ChangeListener changeTilesPerRow = e -> { + // updating label + final int size = sTilesPerRow.getValue(); + lTilesPerRow.setText(String.format(TILES_PER_ROW_LABEL_FMT, size)); + + // updating numRows range + final int numTiles = tis.decoder.getTileCount(); + final int numRows = numTiles / size; + sNumRows.setMaximum(numRows); + + // updating preview + final int width = sTilesPerRow.getValue(); + final int height = sNumRows.getValue(); + lPreviewImage.setIcon(new ImageIcon(tisPreview.get(width, height))); + }; + + /** ChangeListener: JSlider sNumRows */ + private final ChangeListener changeNumRows = e -> { + // updating label + final int size = sNumRows.getValue(); + lNumRows.setText(String.format(ROW_COUNT_LABEL_FMT, size)); + + // updating preview + final int width = sTilesPerRow.getValue(); + final int height = sNumRows.getValue(); + lPreviewImage.setIcon(new ImageIcon(tisPreview.get(width, height))); + }; + + /** ItemListener: JRadioButton rbAuto */ + private final ItemListener itemAuto = e -> { + setPanelEnabled(panelSubAuto, rbAuto.isSelected()); + setPanelEnabled(panelSubManual, !rbAuto.isSelected()); + }; + + /** ItemListener: JRadioButton rbManual */ + private final ItemListener itemManual = e -> { + setPanelEnabled(panelSubAuto, !rbManual.isSelected()); + setPanelEnabled(panelSubManual, rbManual.isSelected()); + }; + + private final ItemListener itemPreviewImageZoom = e -> { + final DataString item = cbPreviewImageZoom.getModel().getElementAt(cbPreviewImageZoom.getSelectedIndex()); + if (item != null) { + final int zoom = item.getData(); + try { + tisPreview.setTileSize(zoom); + } catch (Exception ex) { + ex.printStackTrace(); + // revert zoom change (if possible) + for (int i = 0, size = cbPreviewImageZoom.getModel().getSize(); i < size; i++) { + final int value = cbPreviewImageZoom.getModel().getElementAt(cbPreviewImageZoom.getSelectedIndex()).getData(); + if (value == tisPreview.getTileSize()) { + cbPreviewImageZoom.setSelectedIndex(i); + break; + } + } + } + + final int width = sTilesPerRow.getValue(); + final int height = sNumRows.getValue(); + lPreviewImage.setIcon(new ImageIcon(tisPreview.get(width, height))); + } + }; } } } diff --git a/src/org/infinity/resource/graphics/TisV1Decoder.java b/src/org/infinity/resource/graphics/TisV1Decoder.java index 637d098a7..568b071b1 100644 --- a/src/org/infinity/resource/graphics/TisV1Decoder.java +++ b/src/org/infinity/resource/graphics/TisV1Decoder.java @@ -5,13 +5,13 @@ package org.infinity.resource.graphics; import java.awt.AlphaComposite; -import java.awt.Color; import java.awt.Graphics2D; import java.awt.Image; import java.awt.Transparency; import java.awt.image.BufferedImage; import java.awt.image.DataBufferInt; import java.nio.ByteBuffer; +import java.util.Objects; import org.infinity.resource.key.ResourceEntry; @@ -20,7 +20,6 @@ */ public class TisV1Decoder extends TisDecoder { private static final int HEADER_SIZE = 24; // Size of the TIS header - private static final Color TRANSPARENT_COLOR = new Color(0, true); private ByteBuffer tisBuffer; private int tileCount; @@ -37,12 +36,14 @@ public TisV1Decoder(ResourceEntry tisEntry) { * Returns the palette of the specified tile. * * @param tileIdx The tile index + * @param raw Specifies whether palette entries should be returned unmodified. Otherwise, alpha components are + * added to all palette entries, and "transparent" palette entry is changed to "black". * @return The palette as int array of 256 entries (Format: ARGB). Returns {@code null} on error. */ - public int[] getTilePalette(int tileIdx) { + public int[] getTilePalette(int tileIdx, boolean raw) { if (tileIdx >= 0 && tileIdx < getTileCount()) { int[] palette = new int[256]; - getTilePalette(tileIdx, palette); + getTilePalette(tileIdx, palette, raw); return palette; } else { return null; @@ -54,18 +55,22 @@ public int[] getTilePalette(int tileIdx) { * * @param tileIdx The tile index * @param buffer The buffer to write the palette data into. + * @param raw Specifies whether palette entries should be returned unmodified. Otherwise, alpha components are + * added to all palette entries, and "transparent" palette entry is changed to "black". */ - public void getTilePalette(int tileIdx, int[] buffer) { + public void getTilePalette(int tileIdx, int[] buffer, boolean raw) { if (buffer != null) { int ofs = getTileOffset(tileIdx); if (ofs > 0) { int maxLen = (buffer.length > 256) ? 256 : buffer.length; for (int i = 0; i < maxLen; i++) { buffer[i] = tisBuffer.getInt(ofs); - if (i == 0 && (buffer[i] & 0x00ffffff) == 0x0000ff00) { - buffer[i] &= 0xff000000; - } else { - buffer[i] |= 0xff000000; + if (!raw) { + if (i == 0 && (buffer[i] & 0x00ffffff) == 0x0000ff00) { + buffer[i] &= 0xff000000; + } else { + buffer[i] |= 0xff000000; + } } ofs += 4; } @@ -101,6 +106,7 @@ public void getRawTileData(int tileIdx, byte[] buffer) { if (ofs > 0) { int maxSize = (buffer.length > TILE_DIMENSION * TILE_DIMENSION) ? TILE_DIMENSION * TILE_DIMENSION : buffer.length; + ofs += 4 * 256; // skipping palette data tisBuffer.position(ofs); tisBuffer.get(buffer, 0, maxSize); } @@ -216,8 +222,8 @@ private boolean renderTile(int tileIdx, Image canvas) { buffer = null; Graphics2D g = (Graphics2D) canvas.getGraphics(); try { - g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC)); - g.setColor(TRANSPARENT_COLOR); + g.setComposite(AlphaComposite.Src); + g.setColor(ColorConvert.TRANSPARENT_COLOR); g.fillRect(0, 0, TILE_DIMENSION, TILE_DIMENSION); g.drawImage(workingCanvas, 0, 0, null); } finally { @@ -238,7 +244,7 @@ private boolean renderTile(int tileIdx, int[] buffer) { int ofs = getTileOffset(tileIdx); if (ofs > 0) { ofs += 1024; // skipping palette data - getTilePalette(tileIdx, workingPalette); + getTilePalette(tileIdx, workingPalette, false); for (int i = 0; i < size; i++, ofs++) { buffer[i] = workingPalette[tisBuffer.get(ofs) & 0xff]; } @@ -247,4 +253,33 @@ private boolean renderTile(int tileIdx, int[] buffer) { } return false; } + + @Override + public int hashCode() { + final int prime = 31; + int result = super.hashCode(); + result = prime * result + Objects.hash(tileCount, tileSize, tisBuffer); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!super.equals(obj)) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + TisV1Decoder other = (TisV1Decoder)obj; + return tileCount == other.tileCount && tileSize == other.tileSize && Objects.equals(tisBuffer, other.tisBuffer); + } + + @Override + public String toString() { + return "TisV1Decoder [type=" + getType() + ", tisEntry=" + getResourceEntry() + ", tileCount=" + tileCount + + ", tileSize=" + tileSize + "]"; + } } diff --git a/src/org/infinity/resource/graphics/TisV2Decoder.java b/src/org/infinity/resource/graphics/TisV2Decoder.java index 45c992848..dcbb15440 100644 --- a/src/org/infinity/resource/graphics/TisV2Decoder.java +++ b/src/org/infinity/resource/graphics/TisV2Decoder.java @@ -11,6 +11,7 @@ import java.awt.image.BufferedImage; import java.awt.image.DataBufferInt; import java.nio.ByteBuffer; +import java.util.Objects; import org.infinity.resource.ResourceFactory; import org.infinity.resource.key.ResourceEntry; @@ -197,7 +198,7 @@ private boolean renderTile(int tileIdx, Image canvas) { if (updateWorkingCanvas(tileIdx)) { Graphics2D g = (Graphics2D) canvas.getGraphics(); try { - g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC)); + g.setComposite(AlphaComposite.Src); g.drawImage(workingCanvas, 0, 0, null); } finally { g.dispose(); @@ -259,4 +260,34 @@ private boolean updateWorkingCanvas(int tileIdx) { } return false; } + + @Override + public int hashCode() { + final int prime = 31; + int result = super.hashCode(); + result = prime * result + Objects.hash(pvrzNameBase, tileCount, tileSize, tisBuffer); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!super.equals(obj)) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + TisV2Decoder other = (TisV2Decoder)obj; + return Objects.equals(pvrzNameBase, other.pvrzNameBase) && tileCount == other.tileCount + && tileSize == other.tileSize && Objects.equals(tisBuffer, other.tisBuffer); + } + + @Override + public String toString() { + return "TisV2Decoder [type=" + getType() + ", tisEntry=" + getResourceEntry() + ", tileCount=" + tileCount + + ", tileSize=" + tileSize + ", pvrzNameBase=" + pvrzNameBase + "]"; + } } diff --git a/src/org/infinity/resource/graphics/decoder/DxtDecoder.java b/src/org/infinity/resource/graphics/decoder/DxtDecoder.java index f6328014b..acdaa6f17 100644 --- a/src/org/infinity/resource/graphics/decoder/DxtDecoder.java +++ b/src/org/infinity/resource/graphics/decoder/DxtDecoder.java @@ -102,7 +102,7 @@ private boolean decodeDXT(BufferedImage image, Rectangle region) throws Exceptio if (alignedImage != null) { Graphics2D g = image.createGraphics(); try { - g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC)); + g.setComposite(AlphaComposite.Src); g.drawImage(alignedImage, 0, 0, region.width, region.height, region.x, region.y, region.x + region.width, region.y + region.height, null); } finally { diff --git a/src/org/infinity/resource/graphics/decoder/Etc2Decoder.java b/src/org/infinity/resource/graphics/decoder/Etc2Decoder.java index 5c0ab0262..41c95de33 100644 --- a/src/org/infinity/resource/graphics/decoder/Etc2Decoder.java +++ b/src/org/infinity/resource/graphics/decoder/Etc2Decoder.java @@ -206,7 +206,7 @@ private boolean decodeETC(BufferedImage image, Rectangle region) throws Exceptio if (alignedImage != null) { Graphics2D g = image.createGraphics(); try { - g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC)); + g.setComposite(AlphaComposite.Src); g.drawImage(alignedImage, 0, 0, region.width, region.height, region.x, region.y, region.x + region.width, region.y + region.height, null); } finally { diff --git a/src/org/infinity/resource/graphics/decoder/PvrtcDecoder.java b/src/org/infinity/resource/graphics/decoder/PvrtcDecoder.java index fd7e1c1c0..23610ef1b 100644 --- a/src/org/infinity/resource/graphics/decoder/PvrtcDecoder.java +++ b/src/org/infinity/resource/graphics/decoder/PvrtcDecoder.java @@ -173,7 +173,7 @@ private boolean decodePVRT(BufferedImage image, Rectangle region, boolean is2bpp if (alignedImage != null) { Graphics2D g = image.createGraphics(); try { - g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC)); + g.setComposite(AlphaComposite.Src); g.drawImage(alignedImage, 0, 0, region.width, region.height, region.x, region.y, region.x + region.width, region.y + region.height, null); } finally { diff --git a/src/org/infinity/resource/key/BIFFResourceEntry.java b/src/org/infinity/resource/key/BIFFResourceEntry.java index ebc2e9d5c..5368c2fa7 100644 --- a/src/org/infinity/resource/key/BIFFResourceEntry.java +++ b/src/org/infinity/resource/key/BIFFResourceEntry.java @@ -59,6 +59,16 @@ public BIFFResourceEntry(BIFFEntry bifEntry, String resourceName, int offset) { } } + public BIFFResourceEntry(BIFFResourceEntry entry, boolean hasOverride) { + Objects.requireNonNull(entry); + this.keyFile = entry.keyFile; + this.resourceName = entry.resourceName; + this.type = entry.type; + this.extension = entry.extension; + this.locator = entry.locator; + this.hasOverride = hasOverride; + } + BIFFResourceEntry(Path keyFile, ByteBuffer buffer, int offset) { if (keyFile == null || buffer == null) { throw new NullPointerException("Path to KEY file and byte buffer with BIFF content must not be null"); @@ -192,7 +202,7 @@ public int getLocator() { @Override public ByteBuffer getResourceBuffer(boolean ignoreOverride) throws Exception { - if (!ignoreOverride) { + if (!ignoreOverride && hasOverride) { List overrides = Profile.getOverrideFolders(false); Path file = FileManager.query(overrides, getResourceName()); if (file != null && FileEx.create(file).isFile()) { diff --git a/src/org/infinity/resource/key/ResourceEntry.java b/src/org/infinity/resource/key/ResourceEntry.java index e67976704..818313e37 100644 --- a/src/org/infinity/resource/key/ResourceEntry.java +++ b/src/org/infinity/resource/key/ResourceEntry.java @@ -266,6 +266,12 @@ public boolean isVisible() { public abstract InputStream getResourceDataAsStream(boolean ignoreOverride) throws Exception; + /** + * Returns basic information about the resource. + * + * @param ignoreOverride Indicates whether to retrieve data from the biffed resource version. + * @return For TIS resources: tile count and size of a tile structure. Everything else: Resource size, in bytes. + */ public abstract int[] getResourceInfo(boolean ignoreOverride) throws Exception; /** Returns the full resource name (name dot extension) */ @@ -286,4 +292,22 @@ public boolean isSound() { return extension.equals("WAV") || extension.equals("MUS") || extension.equals("ACM"); } + + /** + * A static method that returns whether any of the listed file extensions supports sound playback. + * + * @param extensions Array of file extensions to check (without leading dot). + * @return {@code true} if any of the file extensions supports sound playback or when an empty extension list is + * provided. {@code false} otherwise. + */ + public static boolean isSound(String... extensions) { + boolean retVal = (extensions.length == 0); + + for (final String ext : extensions) { + retVal |= (ext != null) && (ext.isEmpty() || ext.equalsIgnoreCase("WAV") || ext.equalsIgnoreCase("MUS") || + ext.equalsIgnoreCase("ACM")); + } + + return retVal; + } } diff --git a/src/org/infinity/resource/key/ResourceTreeFolder.java b/src/org/infinity/resource/key/ResourceTreeFolder.java index e0032c25d..3abb8922f 100644 --- a/src/org/infinity/resource/key/ResourceTreeFolder.java +++ b/src/org/infinity/resource/key/ResourceTreeFolder.java @@ -212,6 +212,17 @@ public synchronized boolean retainAll(Collection c) { return bRet; } + /** + * @apiNote Stub method required to meet JDK 21 requirements which introduces their own {@code reversed()} method + * to the {@link SortedSet} and {@link List} interfaces. + * + * TODO: Remove when upgrading source compatibility to JDK 21+ + */ + @SuppressWarnings("unused") + public SortedListSet reversed() { + throw new UnsupportedOperationException(); + } + /** * Replaces the element at the specified position in this set with the specified one. The new element will be moved * to the correct location to preserve the sorted state of the list. diff --git a/src/org/infinity/resource/mus/Entry.java b/src/org/infinity/resource/mus/Entry.java index 0271ba6e3..e52b6dc96 100644 --- a/src/org/infinity/resource/mus/Entry.java +++ b/src/org/infinity/resource/mus/Entry.java @@ -95,11 +95,25 @@ private static long getMaxCacheSize() { } } + private static String getNormalizedLine(String line) { + String retVal = line; + if (retVal != null) { + // removing comment if present + int pos = retVal.indexOf('#'); + if (pos >= 0) { + retVal = retVal.substring(0, pos); + } + } else { + retVal = ""; + } + return retVal; + } + public Entry(ResourceEntry entry, String dir, List entries, String line, int nr) { this.entry = entry; this.dir = dir; this.entryList = entries; - this.line = line; + this.line = getNormalizedLine(line); this.nextnr = nr + 1; } @@ -139,6 +153,9 @@ public void init() throws IOException { } else { endBuffer = getAudioBuffer(next); } + } else if (command.startsWith("#")) { + // comment starts; no more valid tokens available + break; } else { if (command.equalsIgnoreCase(dir)) { command = st.nextToken(); diff --git a/src/org/infinity/resource/mus/Viewer.java b/src/org/infinity/resource/mus/Viewer.java index 4dac4b507..5025189d5 100644 --- a/src/org/infinity/resource/mus/Viewer.java +++ b/src/org/infinity/resource/mus/Viewer.java @@ -160,15 +160,15 @@ private boolean parseMusFile(MusResource mus) { bPlay.setEnabled(false); list.setEnabled(false); StringTokenizer tokenizer = new StringTokenizer(mus.getText(), "\r\n"); - String dir = tokenizer.nextToken().trim(); + String dir = getNextToken(tokenizer, true); listModel.clear(); entryList.clear(); - int count = Integer.valueOf(tokenizer.nextToken().trim()); + int count = Integer.valueOf(getNextToken(tokenizer, true)); for (int i = 0; i < count; i++) { if (isClosed()) { return false; } - Entry entry = new Entry(mus.getResourceEntry(), dir, entryList, tokenizer.nextToken().trim(), i); + Entry entry = new Entry(mus.getResourceEntry(), dir, entryList, getNextToken(tokenizer, true), i); entryList.add(entry); listModel.addElement(entry); } @@ -196,6 +196,24 @@ private boolean parseMusFile(MusResource mus) { return false; } + /** + * Returns the next valid token from the given {@code StringTokenizer}. + * + * @param tokenizer {@link StringTokenizer} containing string tokens. + * @param ignoreComments Whether comments should be skipped. + * @return The next string token if available, an empty string otherwise. + */ + private String getNextToken(StringTokenizer tokenizer, boolean ignoreComments) { + String retVal = ""; + while (tokenizer != null && tokenizer.hasMoreTokens()) { + retVal = tokenizer.nextToken().trim(); + if (!ignoreComments || !retVal.startsWith("#")) { + break; + } + } + return retVal; + } + private void initGUI() { bPlay = new JButton(); setPlayButtonState(false); diff --git a/src/org/infinity/resource/sav/SavResource.java b/src/org/infinity/resource/sav/SavResource.java index 56e136bab..8b0fefcf2 100644 --- a/src/org/infinity/resource/sav/SavResource.java +++ b/src/org/infinity/resource/sav/SavResource.java @@ -120,9 +120,15 @@ public void actionPerformed(ActionEvent event) { } else if (buttonPanel.getControlByType(CTRL_DECOMPRESS) == event.getSource()) { decompressData(true); } else if (buttonPanel.getControlByType(CTRL_EDIT) == event.getSource()) { - ResourceEntry fileentry = entries.get(filelist.getSelectedIndex()); - Resource res = ResourceFactory.getResource(fileentry); - new ViewFrame(panel.getTopLevelAncestor(), res); + try { + ResourceEntry fileentry = entries.get(filelist.getSelectedIndex()); + Resource res = ResourceFactory.getResource(fileentry); + new ViewFrame(panel.getTopLevelAncestor(), res); + } catch (Exception e) { + e.printStackTrace(); + JOptionPane.showMessageDialog(panel.getTopLevelAncestor(), "Could not open selected resource.", "Error", + JOptionPane.ERROR_MESSAGE); + } } else if (buttonPanel.getControlByType(ButtonPanel.Control.EXPORT_BUTTON) == event.getSource()) { ResourceFactory.exportResource(entry, panel.getTopLevelAncestor()); } else if (buttonPanel.getControlByType(CTRL_DELETE) == event.getSource()) { @@ -209,9 +215,15 @@ public JComponent makeViewer(ViewableContainer container) { @Override public void mouseClicked(MouseEvent event) { if (event.getClickCount() == 2) { - ResourceEntry fileentry = entries.get(filelist.getSelectedIndex()); - Resource res = ResourceFactory.getResource(fileentry); - new ViewFrame(panel.getTopLevelAncestor(), res); + try { + ResourceEntry fileentry = entries.get(filelist.getSelectedIndex()); + Resource res = ResourceFactory.getResource(fileentry); + new ViewFrame(panel.getTopLevelAncestor(), res); + } catch (Exception e) { + e.printStackTrace(); + JOptionPane.showMessageDialog(panel.getTopLevelAncestor(), "Could not open selected resource.", "Error", + JOptionPane.ERROR_MESSAGE); + } } } }); diff --git a/src/org/infinity/resource/text/PlainTextResource.java b/src/org/infinity/resource/text/PlainTextResource.java index fbe77f4c9..3c31e082d 100644 --- a/src/org/infinity/resource/text/PlainTextResource.java +++ b/src/org/infinity/resource/text/PlainTextResource.java @@ -626,6 +626,10 @@ private void setSyntaxHighlightingEnabled(InfinityTextArea edit, InfinityScrollP if (!BrowserMenuBar.isInstantiated() || BrowserMenuBar.getInstance().getOptions().getLuaSyntaxHighlightingEnabled()) { language = InfinityTextArea.Language.LUA; } + } else if ("MENU".equalsIgnoreCase(entry.getExtension())) { + if (!BrowserMenuBar.isInstantiated() || BrowserMenuBar.getInstance().getOptions().getMenuSyntaxHighlightingEnabled()) { + language = InfinityTextArea.Language.MENU; + } } else if ("INI".equalsIgnoreCase(entry.getExtension())) { if (!BrowserMenuBar.isInstantiated() || BrowserMenuBar.getInstance().getOptions().getIniSyntaxHighlightingEnabled()) { language = InfinityTextArea.Language.INI; diff --git a/src/org/infinity/resource/text/modes/MenuTokenMaker.flex b/src/org/infinity/resource/text/modes/MenuTokenMaker.flex new file mode 100644 index 000000000..845c787c3 --- /dev/null +++ b/src/org/infinity/resource/text/modes/MenuTokenMaker.flex @@ -0,0 +1,673 @@ +// Near Infinity - An Infinity Engine Browser and Editor +// Copyright (C) 2001 Jon Olav Hauglid +// See LICENSE.txt for license information + +/* + * 22/04/2024 + * + * MenuTokenMaker.java - Modified version of LuaTokenMaker.java that provides + * additional supported for Menu-specific scripting. + * + * This library is distributed under a modified BSD license. See the included + * LICENSE file for details. + */ +package org.infinity.resource.text.modes; + +import java.io.*; +import javax.swing.text.Segment; + +import org.fife.ui.rsyntaxtextarea.*; + + +/** + * Scanner for the Menu scripting language extension.

+ * + * This implementation was created using + * JFlex 1.4.1; however, the generated file + * was modified for performance. Memory allocation needs to be almost + * completely removed to be competitive with the handwritten lexers (subclasses + * of AbstractTokenMaker), so this class has been modified so that + * Strings are never allocated (via yytext()), and the scanner never has to + * worry about refilling its buffer (needlessly copying chars around). + * We can achieve this because RText always scans exactly 1 line of tokens at a + * time, and hands the scanner this line as an array of characters (a Segment + * really). Since tokens contain pointers to char arrays instead of Strings + * holding their contents, there is no need for allocating new memory for + * Strings.

+ * + * The actual algorithm generated for scanning has, of course, not been + * modified.

+ * + * If you wish to regenerate this file yourself, keep in mind the following: + *

    + *
  • The generated MenuTokenMaker.java file will contain two + * definitions of both zzRefill and yyreset. + * You should hand-delete the second of each definition (the ones + * generated by the lexer), as these generated methods modify the input + * buffer, which we'll never have to do.
  • + *
  • You should also change the declaration/definition of zzBuffer to NOT + * be initialized. This is a needless memory allocation for us since we + * will be pointing the array somewhere else anyway.
  • + *
  • You should NOT call yylex() on the generated scanner + * directly; rather, you should use getTokenList as you would + * with any other TokenMaker instance.
  • + *
+ * + * @author Robert Futrell + * @version 0.4 + * + */ +%% + +%public +%class MenuTokenMaker +%extends AbstractJFlexTokenMaker +%unicode +%ignorecase +%type org.fife.ui.rsyntaxtextarea.Token + + +%{ + + /** Style for highlighting MENU scripts. */ + public static final String SYNTAX_STYLE_MENU = "text/MENU"; + + /** + * Constructor. This must be here because JFlex does not generate a + * no-parameter constructor. + */ + public MenuTokenMaker() { + } + + + /** + * Adds the token specified to the current linked list of tokens. + * + * @param tokenType The token's type. + */ + private void addToken(int tokenType) { + addToken(zzStartRead, zzMarkedPos-1, tokenType); + } + + + /** + * Adds the token specified to the current linked list of tokens. + * + * @param tokenType The token's type. + */ + private void addToken(int start, int end, int tokenType) { + int so = start + offsetShift; + addToken(zzBuffer, start,end, tokenType, so); + } + + + /** + * Adds the token specified to the current linked list of tokens. + * + * @param array The character array. + * @param start The starting offset in the array. + * @param end The ending offset in the array. + * @param tokenType The token's type. + * @param startOffset The offset in the document at which this token + * occurs. + */ + @Override + public void addToken(char[] array, int start, int end, int tokenType, int startOffset) { + super.addToken(array, start,end, tokenType, startOffset); + zzStartRead = zzMarkedPos; + } + + + @Override + public String[] getLineCommentStartAndEnd(int languageIndex) { + return new String[] { "--", null }; + } + + + /** + * Returns the first token in the linked list of tokens generated + * from text. This method must be implemented by + * subclasses so they can correctly implement syntax highlighting. + * + * @param text The text from which to get tokens. + * @param initialTokenType The token type we should start with. + * @param startOffset The offset into the document at which + * text starts. + * @return The first Token in a linked list representing + * the syntax highlighted text. + */ + public Token getTokenList(Segment text, int initialTokenType, int startOffset) { + + resetTokenList(); + this.offsetShift = -text.offset + startOffset; + + // Start off in the proper state. + int state = Token.NULL; + switch (initialTokenType) { + case Token.COMMENT_MULTILINE: + state = MLC; + start = text.offset; + break; + case Token.LITERAL_BACKQUOTE: + state = LONGSTRING; + start = text.offset; + break; + case Token.LITERAL_STRING_DOUBLE_QUOTE: + state = STRING; + start = text.offset; + break; + default: + state = Token.NULL; + } + + s = text; + try { + yyreset(zzReader); + yybegin(state); + return yylex(); + } catch (IOException ioe) { + ioe.printStackTrace(); + return new TokenImpl(); + } + + } + + + /** + * Refills the input buffer. + * + * @return true if EOF was reached, otherwise + * false. + */ + private boolean zzRefill() { + return zzCurrentPos>=s.offset+s.count; + } + + + /** + * Resets the scanner to read from a new input stream. + * Does not close the old reader. + * + * All internal variables are reset, the old input stream + * cannot be reused (internal buffer is discarded and lost). + * Lexical state is set to YY_INITIAL. + * + * @param reader the new input stream + */ + public final void yyreset(Reader reader) { + // 's' has been updated. + zzBuffer = s.array; + /* + * We replaced the line below with the two below it because zzRefill + * no longer "refills" the buffer (since the way we do it, it's always + * "full" the first time through, since it points to the segment's + * array). So, we assign zzEndRead here. + */ + //zzStartRead = zzEndRead = s.offset; + zzStartRead = s.offset; + zzEndRead = zzStartRead + s.count - 1; + zzCurrentPos = zzMarkedPos = zzPushbackPos = s.offset; + zzLexicalState = YYINITIAL; + zzReader = reader; + zzAtBOL = true; + zzAtEOF = false; + } + + +%} + +Letter = [A-Za-z_] +Digit = [0-9] + +LineTerminator = (\n) +WhiteSpace = ([ \t\f]) + +UnclosedCharLiteral = ([\']([^\'\n]|"\\'")*) +CharLiteral = ({UnclosedCharLiteral}"'") + +StringDelimiter = ([\"]) + +LongStringBegin = ("[[") +LongStringEnd = ("]]") + +LineCommentBegin = ("--") +MLCBegin = ({LineCommentBegin}{LongStringBegin}) + +Number = ( "."? {Digit} ({Digit}|".")* ([eE][+-]?)? ({Letter}|{Digit})* ) +BooleanLiteral = ("true"|"false") + +Separator = ([\(\)\{\}\[\]\]]) +Separator2 = ([\;:,.]) + +ArithmeticOperator = ("+"|"-"|"*"|"/"|"^"|"%") +RelationalOperator = ("<"|">"|"<="|">="|"=="|"~=") +LogicalOperator = ("and"|"or"|"not"|"#") +ConcatenationOperator = ("..") +Elipsis = ({ConcatenationOperator}".") +Operator = ({ArithmeticOperator}|{RelationalOperator}|{LogicalOperator}|{ConcatenationOperator}|{Elipsis}) + +Identifier = ({Letter}({Letter}|{Digit})*) + +LuaBlockDelimiter = ([`]) + + +%state MLC +%state LONGSTRING +%state STRING +%state LINECOMMENT + + +%% + +/* Lua keywords */ + "break" | + "do" | + "else" | + "elseif" | + "end" | + "for" | + "function" | + "goto" | + "if" | + "in" | + "local" | + "nil" | + "repeat" | + "return" | + "then" | + "until" | + "while" { addToken(Token.RESERVED_WORD); } + +/* Menu keywords */ + "action" | + "actionHold" | + "actionScroll" | + "actionUpdate" | + "actionalt" | + "actionbar" | + "actiondbl" | + "actiondrag" | + "actionenter" | + "actionexit" | + "actionsimpledrag" | + "actionsimpledrop" | + "align" | + "area" | + "areamap" | + "background" | + "bam" | + "bitmap" | + "bottom" | + "boundingPoints" | + "button" | + "center" | + "clickable" | + "clunkyScroll" | + "color" | + "colordisplay" | + "column" | + "count" | + "dynamic" | + "edit" | + "enabled" | + "encumbrance" | + "escape" | + "exclusiveFocus" | + "fill" | + "focus" | + "font" | + "force" | + "frame" | + "frameTimes" | + "full" | + "func" | + "glow" | + "greyscale" | + "halign" | + "handle" | + "healthbar" | + "height" | + "hide" | + "hidehighlight" | + "highlight" | + "highlightgroup" | + "icon" | + "ignoreEsc" | + "ignoreEvents" | + "ignoreFocus" | + "ignoreNav" | + "indent" | + "interactable" | + "item" | + "label" | + "left" | + "list" | + "loop" | + "lower" | + "lua" | + "map" | + "maxchars" | + "maxlines" | + "memorizedSpellInfo" | + "menu" | + "modal" | + "mosaic" | + "movie" | + "name" | + "noBorder" | + "offset" | + "on" | + "onclose" | + "onopen" | + "opacity" | + "overlayTint" | + "pad" | + "pagedown" | + "pageup" | + "palette" | + "panel" | + "paperdoll" | + "placeholder" | + "point" | + "popupSound" | + "portrait" | + "position" | + "progressbar" | + "pulse" | + "queuedMovie" | + "rectangle" | + "respectClipping" | + "respectConstraints" | + "right" | + "rowbackground" | + "rowheight" | + "rowwidth" | + "scaleToClip" | + "scrollbar" | + "seperator" | + "sequence" | + "sequenceonce" | + "settings" | + "shadow" | + "size" | + "skipReset" | + "slider" | + "sliderBackground" | + "slot" | + "slotinfo" | + "sound" | + "spellInfo" | + "state" | + "style" | + "subtitle" | + "table" | + "template" | + "text" | + "tint" | + "toggle" | + "tooltip" | + "top" | + "transparent" | + "upper" | + "usages" | + "useFontZoom" | + "useOverlayTint" | + "usealpha" | + "valid" | + "valign" | + "var" | + "width" | + "worldmap" { addToken(Token.RESERVED_WORD_2); } + +/* Data types. */ + "" | + "" | + "" | + "" | + "NULL" { addToken(Token.DATA_TYPE); } + +/* Lua functions. */ + "_G" | + "_VERSION" | + "assert" | + "collectgarbage" | + "dofile" | + "error" | + "getfenv" | + "getmetatable" | + "ipairs" | + "load" | + "loadfile" | + "loadstring" | + "module" | + "next" | + "pairs" | + "pcall" | + "print" | + "rawequal" | + "rawget" | + "rawset" | + "require" | + "select" | + "setfenv" | + "setmetatable" | + "tonumber" | + "tostring" | + "type" | + "unpack" | + "xpcall" | +/* Infinity Engine functions */ + "Infinity_ActivateInventory" | + "Infinity_ActivateRecord" | + "Infinity_AddDLC" | + "Infinity_AddDLCContent" | + "Infinity_CanCloudSave" | + "Infinity_CanLevelUp" | + "Infinity_ChangeOption" | + "Infinity_CheckItemIdentify" | + "Infinity_ClickItem" | + "Infinity_ClickObjectInWorld" | + "Infinity_ClickScreen" | + "Infinity_ClickWorldAt" | + "Infinity_CloseEngine" | + "Infinity_DestroyAnimation" | + "Infinity_DisplayString" | + "Infinity_DoFile" | + "Infinity_EnterEdit" | + "Infinity_FetchString" | + "Infinity_FindItemWithBam" | + "Infinity_FindItemWithText" | + "Infinity_FindUIItemByName" | + "Infinity_FocusTextEdit" | + "Infinity_GetClockTicks" | + "Infinity_GetContainerItemDescription" | + "Infinity_GetContentHeight" | + "Infinity_GetCurrentGroundPage" | + "Infinity_GetCurrentMovie" | + "Infinity_GetCurrentScreenName" | + "Infinity_GetFilesOfType" | + "Infinity_GetFrameCounter" | + "Infinity_GetGameTicks" | + "Infinity_GetGroundItemDescription" | + "Infinity_GetGroupItemDescription" | + "Infinity_GetINIString" | + "Infinity_GetINIValue" | + "Infinity_GetInCutsceneMode" | + "Infinity_GetListHeight" | + "Infinity_GetMaxChapterPage" | + "Infinity_GetMaxGroundPage" | + "Infinity_GetMenuArea" | + "Infinity_GetMenuItemByName" | + "Infinity_GetMousePosition" | + "Infinity_GetNumCharacters" | + "Infinity_GetOffset" | + "Infinity_GetOption" | + "Infinity_GetPasswordRequired" | + "Infinity_GetPortraitTooltip" | + "Infinity_GetScreenSize" | + "Infinity_GetScriptVarInt" | + "Infinity_GetScrollIdentifyEnabled" | + "Infinity_GetSelectedCharacterName" | + "Infinity_GetSpellIdentifyEnabled" | + "Infinity_GetTimeString" | + "Infinity_GetTransitionInProgress" | + "Infinity_GetUseButtonText" | + "Infinity_GooglePlaySignedIn" | + "Infinity_HighlightJournalButton" | + "Infinity_HoverMouseOver" | + "Infinity_HoverMouseOverObject" | + "Infinity_InstanceAnimation" | + "Infinity_IsItemEnabled" | + "Infinity_IsMenuOnStack" | + "Infinity_IsPlayerMoving" | + "Infinity_JoinMultiplayerGame" | + "Infinity_LaunchURL" | + "Infinity_LevelUp" | + "Infinity_Log" | + "Infinity_LookAtObjectInWorld" | + "Infinity_LuaConsoleInput" | + "Infinity_OnAddUserEntry" | + "Infinity_OnCharacterImportItemSelect" | + "Infinity_OnCharacterItemSelect" | + "Infinity_OnEditUserEntry" | + "Infinity_OnGroundPage" | + "Infinity_OnPortraitDblClick" | + "Infinity_OnPortraitItemSelect" | + "Infinity_OnPortraitLClick" | + "Infinity_OnPortraitRClick" | + "Infinity_OnRemoveUserEntry" | + "Infinity_OnScriptItemSelect" | + "Infinity_OnScrollIdentify" | + "Infinity_OnSoundItemSelect" | + "Infinity_OnSpellIdentify" | + "Infinity_OnUseButtonClick" | + "Infinity_OpenInventoryContainer" | + "Infinity_PlayMovie" | + "Infinity_PlaySound" | + "Infinity_PopMenu" | + "Infinity_PressKeyboardButton" | + "Infinity_PushMenu" | + "Infinity_RandomNumber" | + "Infinity_RemoveINIEntry" | + "Infinity_RequestMultiplayerGameDetails" | + "Infinity_ScaleToText" | + "Infinity_ScrollLists" | + "Infinity_SelectDialogueOption" | + "Infinity_SelectItemAbility" | + "Infinity_SelectListItem" | + "Infinity_SendChatMessage" | + "Infinity_SetArea" | + "Infinity_SetBackground" | + "Infinity_SetCloudEnabled" | + "Infinity_SetGooglePlaySigninState" | + "Infinity_SetHairColor" | + "Infinity_SetHighlightColors" | + "Infinity_SetINIValue" | + "Infinity_SetKey" | + "Infinity_SetLanguage" | + "Infinity_SetMajorColor" | + "Infinity_SetMinorColor" | + "Infinity_SetOffset" | + "Infinity_SetOverlay" | + "Infinity_SetScreenSize" | + "Infinity_SetScrollTop" | + "Infinity_SetSkinColor" | + "Infinity_SetToken" | + "Infinity_ShutdownGame" | + "Infinity_SignInOutButtonEnabled" | + "Infinity_SplitItemStack" | + "Infinity_StartItemCapture" | + "Infinity_StartKeybind" | + "Infinity_StopItemCapture" | + "Infinity_StopKeybind" | + "Infinity_StopMovie" | + "Infinity_SwapSlot" | + "Infinity_SwapWithAppearance" | + "Infinity_SwapWithPortrait" | + "Infinity_TakeScreenshot" | + "Infinity_TextEditHasFocus" | + "Infinity_TransitionMenu" | + "Infinity_UpdateCharacterRecordExportPanel" | + "Infinity_UpdateCloudSaveState" | + "Infinity_UpdateInventoryRequesterPanel" | + "Infinity_UpdateLuaStats" | + "Infinity_UpdateStoreMainPanel" | + "Infinity_UpdateStoreRequesterPanel" | + "Infinity_WriteINILine" { addToken(Token.FUNCTION); } + +/* Booleans */ + {BooleanLiteral} { addToken(Token.LITERAL_BOOLEAN); } + + + { + + {LineTerminator} { addNullToken(); return firstToken; } + + {WhiteSpace}+ { addToken(Token.WHITESPACE); } + + /* String/Character literals. */ + {CharLiteral} { addToken(Token.LITERAL_CHAR); } + {UnclosedCharLiteral} { addToken(Token.ERROR_CHAR); addNullToken(); return firstToken; } + + {StringDelimiter} { start = zzMarkedPos-1; yybegin(STRING); } + + {LongStringBegin} { start = zzMarkedPos-2; yybegin(LONGSTRING); } + + /* Comment literals. */ + {MLCBegin} { start = zzMarkedPos-4; yybegin(MLC); } + {LineCommentBegin} { start = zzMarkedPos-2; yybegin(LINECOMMENT); } + + /* Separators. */ + {Separator} { addToken(Token.SEPARATOR); } + {Separator2} { addToken(Token.IDENTIFIER); } + + /* Operators. */ + {Operator} { addToken(Token.OPERATOR); } + + /* Identifiers - Comes after Operators for "and", "not" and "or". */ + {Identifier} { addToken(Token.IDENTIFIER); } + + /* Numbers */ + {Number} { addToken(Token.LITERAL_NUMBER_FLOAT); } + + /* Lua block delimiter */ + {LuaBlockDelimiter} { addToken(Token.PREPROCESSOR); } + + /* Ended with a line not in a string or comment. */ + <> { addNullToken(); return firstToken; } + + /* Catch any other (unhandled) characters. */ + . { addToken(Token.IDENTIFIER); } + +} + + + { + [^\n\]]+ {} + \n { addToken(start,zzStartRead-1, Token.COMMENT_MULTILINE); return firstToken; } + {LongStringEnd} { yybegin(YYINITIAL); addToken(start,zzStartRead+1, Token.COMMENT_MULTILINE); } + \] {} + <> { addToken(start,zzStartRead-1, Token.COMMENT_MULTILINE); return firstToken; } +} + + + { + [^\n\]]+ {} + \n { addToken(start,zzStartRead-1, Token.LITERAL_BACKQUOTE); return firstToken; } + {LongStringEnd} { yybegin(YYINITIAL); addToken(start,zzStartRead+1, Token.LITERAL_BACKQUOTE); } + \] {} + <> { addToken(start,zzStartRead-1, Token.LITERAL_BACKQUOTE); return firstToken; } +} + + { + [^\"\n]+ {} + {StringDelimiter} { yybegin(YYINITIAL); addToken(start,zzStartRead, Token.LITERAL_STRING_DOUBLE_QUOTE); } + \n { addToken(start,zzStartRead-1, Token.LITERAL_STRING_DOUBLE_QUOTE); return firstToken; } + <> { addToken(start,zzStartRead-1, Token.LITERAL_STRING_DOUBLE_QUOTE); return firstToken; } +} + + { + [^\n]+ {} + \n { addToken(start,zzStartRead-1, Token.COMMENT_EOL); return firstToken; } + <> { addToken(start,zzStartRead-1, Token.COMMENT_EOL); return firstToken; } +} diff --git a/src/org/infinity/resource/text/modes/MenuTokenMaker.java b/src/org/infinity/resource/text/modes/MenuTokenMaker.java new file mode 100644 index 000000000..4db3227f9 --- /dev/null +++ b/src/org/infinity/resource/text/modes/MenuTokenMaker.java @@ -0,0 +1,4834 @@ +// Near Infinity - An Infinity Engine Browser and Editor +// Copyright (C) 2001 Jon Olav Hauglid +// See LICENSE.txt for license information + +/* The following code was generated by JFlex 1.5.1 */ + +/* + * 22/04/2024 + * + * MenuTokenMaker.java - Modified version of LuaTokenMaker.java that provides + * additional supported for Menu-specific scripting. + * + * This library is distributed under a modified BSD license. See the included + * LICENSE file for details. + */ +package org.infinity.resource.text.modes; + +import java.io.IOException; +import java.io.Reader; + +import javax.swing.text.Segment; + +import org.fife.ui.rsyntaxtextarea.AbstractJFlexTokenMaker; +import org.fife.ui.rsyntaxtextarea.Token; +import org.fife.ui.rsyntaxtextarea.TokenImpl; + + +/** + * Scanner for the Menu scripting language extension.

+ * + * This implementation was created using + * JFlex 1.4.1; however, the generated file + * was modified for performance. Memory allocation needs to be almost + * completely removed to be competitive with the handwritten lexers (subclasses + * of AbstractTokenMaker), so this class has been modified so that + * Strings are never allocated (via yytext()), and the scanner never has to + * worry about refilling its buffer (needlessly copying chars around). + * We can achieve this because RText always scans exactly 1 line of tokens at a + * time, and hands the scanner this line as an array of characters (a Segment + * really). Since tokens contain pointers to char arrays instead of Strings + * holding their contents, there is no need for allocating new memory for + * Strings.

+ * + * The actual algorithm generated for scanning has, of course, not been + * modified.

+ * + * If you wish to regenerate this file yourself, keep in mind the following: + *

    + *
  • The generated MenuTokenMaker.java file will contain two + * definitions of both zzRefill and yyreset. + * You should hand-delete the second of each definition (the ones + * generated by the lexer), as these generated methods modify the input + * buffer, which we'll never have to do.
  • + *
  • You should also change the declaration/definition of zzBuffer to NOT + * be initialized. This is a needless memory allocation for us since we + * will be pointing the array somewhere else anyway.
  • + *
  • You should NOT call yylex() on the generated scanner + * directly; rather, you should use getTokenList as you would + * with any other TokenMaker instance.
  • + *
+ * + * @author Robert Futrell + * @version 0.4 + * + */ + +public class MenuTokenMaker extends AbstractJFlexTokenMaker { + + /** This character denotes the end of file */ + public static final int YYEOF = -1; + + /** lexical states */ + public static final int YYINITIAL = 0; + public static final int MLC = 2; + public static final int LONGSTRING = 4; + public static final int STRING = 6; + public static final int LINECOMMENT = 8; + + /** + * ZZ_LEXSTATE[l] is the state in the DFA for the lexical state l + * ZZ_LEXSTATE[l+1] is the state in the DFA for the lexical state l + * at the beginning of a line + * l is of the form l = 2*k, k a non negative integer + */ + private static final int ZZ_LEXSTATE[] = { + 0, 0, 1, 1, 2, 2, 3, 3, 4, 4 + }; + + /** + * Translates characters to character classes + */ + private static final String ZZ_CMAP_PACKED = + "\11\0\1\4\1\3\1\61\1\4\1\61\22\0\1\4\1\0\1\7"+ + "\1\27\1\0\1\27\1\0\1\5\2\26\1\27\1\15\1\0\1\12"+ + "\1\13\1\27\12\2\2\0\1\30\1\32\1\31\2\0\1\22\1\40"+ + "\1\45\1\35\1\14\1\21\1\46\1\50\1\44\1\1\1\42\1\23"+ + "\1\53\1\34\1\36\1\47\1\57\1\17\1\25\1\16\1\20\1\55"+ + "\1\51\1\52\1\54\1\56\1\10\1\6\1\11\1\27\1\60\1\37"+ + "\1\22\1\40\1\45\1\35\1\14\1\21\1\46\1\50\1\44\1\1"+ + "\1\42\1\23\1\53\1\34\1\36\1\47\1\57\1\17\1\25\1\16"+ + "\1\20\1\55\1\51\1\52\1\54\1\56\1\26\1\0\1\26\1\33"+ + "\6\0\1\61\252\0\2\43\115\0\1\24\u1ea8\0\1\61\1\61\u0100\0"+ + "\1\41\uded5\0"; + + /** + * Translates characters to character classes + */ + private static final char [] ZZ_CMAP = zzUnpackCMap(ZZ_CMAP_PACKED); + + /** + * Translates DFA states to action switch labels. + */ + private static final int [] ZZ_ACTION = zzUnpackAction(); + + private static final String ZZ_ACTION_PACKED_0 = + "\5\0\2\1\1\2\1\3\1\4\1\5\1\6\2\7"+ + "\1\10\2\1\1\10\10\1\2\10\4\1\1\11\15\1"+ + "\1\12\1\13\1\12\1\14\2\12\1\15\1\16\1\12"+ + "\1\17\2\2\1\20\1\5\1\21\1\22\1\10\2\1"+ + "\1\0\10\1\1\0\6\1\1\0\1\1\1\0\7\1"+ + "\1\0\3\1\1\0\7\1\1\0\1\1\12\0\12\1"+ + "\3\0\4\1\1\0\1\1\1\23\1\1\1\10\1\1"+ + "\1\24\6\1\1\0\1\1\1\0\2\23\3\0\1\1"+ + "\2\23\22\1\1\0\2\1\1\0\10\1\1\25\1\1"+ + "\1\26\1\27\1\20\1\0\1\1\1\0\1\1\1\0"+ + "\3\1\1\0\6\1\1\0\4\1\1\24\3\1\1\0"+ + "\6\1\1\0\1\1\2\0\11\1\1\23\1\1\1\0"+ + "\1\1\1\0\1\1\1\0\6\1\20\0\17\1\4\0"+ + "\17\1\1\0\1\1\7\0\11\1\1\0\7\1\1\0"+ + "\3\1\1\0\5\1\1\0\5\1\2\0\3\1\1\0"+ + "\1\1\1\0\4\1\1\0\6\1\1\30\1\1\2\23"+ + "\1\0\3\1\1\24\2\1\1\31\1\1\1\0\4\1"+ + "\1\0\5\1\1\0\4\1\4\0\7\1\1\24\1\0"+ + "\1\1\1\24\1\0\1\1\1\0\1\1\1\25\7\0"+ + "\1\24\7\0\7\1\1\24\6\1\4\0\1\32\1\1"+ + "\1\0\2\1\1\0\6\1\1\0\3\1\1\0\1\1"+ + "\3\0\4\1\1\0\6\1\1\0\1\1\1\0\5\1"+ + "\1\0\4\1\1\24\1\0\1\24\2\1\1\0\1\1"+ + "\1\0\4\1\1\0\1\1\1\0\3\1\1\0\5\1"+ + "\1\0\1\1\1\0\3\1\1\0\3\1\1\0\4\1"+ + "\4\0\3\1\1\24\1\1\1\31\1\0\1\1\15\0"+ + "\14\1\1\32\2\0\1\1\2\0\2\1\1\0\2\1"+ + "\4\0\3\1\1\0\1\1\1\0\2\1\1\24\1\0"+ + "\2\1\1\25\4\1\1\0\3\1\2\0\3\1\1\0"+ + "\2\1\1\0\2\1\1\0\2\1\1\0\1\1\1\0"+ + "\1\1\1\0\2\1\1\0\2\1\3\0\3\1\1\0"+ + "\1\1\2\24\6\0\1\24\3\0\6\1\1\24\2\1"+ + "\3\0\2\1\1\0\1\1\1\0\1\1\3\0\3\1"+ + "\1\0\1\1\1\0\2\1\1\0\5\1\2\0\2\1"+ + "\1\0\3\1\1\0\2\1\1\0\1\1\1\0\1\1"+ + "\1\0\1\1\1\0\1\1\3\0\1\1\1\0\1\1"+ + "\10\0\7\1\6\0\6\1\1\0\1\1\5\0\5\1"+ + "\1\0\2\1\1\0\1\1\1\0\2\1\2\0\2\1"+ + "\1\0\2\1\1\0\2\1\1\0\1\1\1\0\1\1"+ + "\2\0\1\1\10\0\5\1\1\0\1\24\2\0\1\1"+ + "\1\24\2\1\1\0\1\1\4\0\5\1\1\0\1\1"+ + "\2\0\2\1\1\0\3\1\1\0\1\1\2\0\2\1"+ + "\7\0\2\1\4\0\1\1\1\0\1\1\3\0\3\1"+ + "\1\0\1\1\1\0\2\24\1\0\1\1\2\0\2\1"+ + "\2\0\1\1\2\0\1\1\22\0\23\1\1\0\1\1"+ + "\1\0\1\1\1\0\1\1\2\0\2\1\2\0\1\1"+ + "\53\0\54\1\1\0\1\1\2\0\2\1\1\0\1\1"+ + "\73\0\73\1\1\0\1\1\2\0\2\1\1\0\1\1"+ + "\122\0\122\1\1\0\1\1\143\0\143\1\14\0\1\25"+ + "\124\0\14\1\1\25\124\1\133\0\132\1\125\0\122\1"+ + "\110\0\105\1\102\0\77\1\71\0\67\1\57\0\55\1"+ + "\44\0\44\1\33\0\1\25\1\0\33\1\1\25\1\1"+ + "\31\0\31\1\21\0\21\1\12\0\12\1\11\0\11\1"+ + "\6\0\6\1\3\0\3\1\3\0\3\1\2\0\2\1"+ + "\2\0\2\1\2\0\2\1\2\0\2\1\2\0\2\1"+ + "\1\0\1\1"; + + private static int [] zzUnpackAction() { + int [] result = new int[2802]; + int offset = 0; + offset = zzUnpackAction(ZZ_ACTION_PACKED_0, offset, result); + return result; + } + + private static int zzUnpackAction(String packed, int offset, int [] result) { + int i = 0; /* index in packed string */ + int j = offset; /* index in unpacked array */ + int l = packed.length(); + while (i < l) { + int count = packed.charAt(i++); + int value = packed.charAt(i++); + do result[j++] = value; while (--count > 0); + } + return j; + } + + + /** + * Translates a state to a row index in the transition table + */ + private static final int [] ZZ_ROWMAP = zzUnpackRowMap(); + + private static final String ZZ_ROWMAP_PACKED_0 = + "\0\0\0\62\0\144\0\226\0\310\0\372\0\u012c\0\u015e"+ + "\0\372\0\u0190\0\u01c2\0\372\0\u01f4\0\372\0\u0226\0\u0258"+ + "\0\u028a\0\372\0\u02bc\0\u02ee\0\u0320\0\u0352\0\u0384\0\u03b6"+ + "\0\u03e8\0\u041a\0\u044c\0\u047e\0\u047e\0\u04b0\0\u04e2\0\u0514"+ + "\0\372\0\u0546\0\u0578\0\u05aa\0\u05dc\0\u060e\0\u0640\0\u0672"+ + "\0\u06a4\0\u06d6\0\u0708\0\u073a\0\u076c\0\u079e\0\u07d0\0\372"+ + "\0\u0802\0\372\0\u0834\0\u0866\0\372\0\372\0\u0898\0\372"+ + "\0\u08ca\0\u08fc\0\372\0\u092e\0\372\0\u0960\0\u0992\0\u09c4"+ + "\0\u09f6\0\u0a28\0\u0a5a\0\u0a8c\0\u0abe\0\u0af0\0\u0b22\0\u0b54"+ + "\0\u0b86\0\u0bb8\0\u0bea\0\u0c1c\0\u0c4e\0\u0c80\0\u0cb2\0\u0ce4"+ + "\0\u0d16\0\u0d48\0\u0d7a\0\u0dac\0\u0dde\0\u0e10\0\u0e42\0\u0e74"+ + "\0\u0ea6\0\u0ed8\0\u0f0a\0\u0f3c\0\u0f6e\0\u0fa0\0\u0fd2\0\u1004"+ + "\0\u1036\0\u1068\0\u109a\0\u10cc\0\u10fe\0\u1130\0\u1162\0\u1194"+ + "\0\u11c6\0\u11f8\0\u122a\0\u125c\0\u128e\0\u12c0\0\u12f2\0\u1324"+ + "\0\u1356\0\u1388\0\u13ba\0\u13ec\0\u141e\0\u1450\0\u1482\0\u14b4"+ + "\0\u14e6\0\u1518\0\u154a\0\u157c\0\u15ae\0\u15e0\0\u1612\0\u1644"+ + "\0\u1676\0\u16a8\0\u16da\0\u170c\0\u173e\0\u1770\0\u17a2\0\u17d4"+ + "\0\u012c\0\u1806\0\u1838\0\u186a\0\u189c\0\u18ce\0\u1900\0\u1932"+ + "\0\u1964\0\u1996\0\u19c8\0\u19fa\0\372\0\u1a2c\0\u1a5e\0\u1a90"+ + "\0\u1ac2\0\u1af4\0\u012c\0\u1b26\0\u1b58\0\u1b8a\0\u1bbc\0\u1bee"+ + "\0\u1c20\0\u1c52\0\u1c84\0\u1cb6\0\u1ce8\0\u1d1a\0\u1d4c\0\u1d7e"+ + "\0\u1db0\0\u1de2\0\u1e14\0\u1e46\0\u1e78\0\u1eaa\0\u1edc\0\u1f0e"+ + "\0\u1f40\0\u1f72\0\u1fa4\0\u1fd6\0\u2008\0\u203a\0\u206c\0\u209e"+ + "\0\u20d0\0\u2102\0\u012c\0\u2134\0\372\0\372\0\u01c2\0\u2166"+ + "\0\u2198\0\u21ca\0\u21fc\0\u222e\0\u2260\0\u2292\0\u22c4\0\u22f6"+ + "\0\u2328\0\u235a\0\u238c\0\u23be\0\u23f0\0\u2422\0\u2454\0\u2486"+ + "\0\u24b8\0\u24ea\0\u251c\0\u012c\0\u254e\0\u2580\0\u25b2\0\u25e4"+ + "\0\u2616\0\u2648\0\u267a\0\u26ac\0\u26de\0\u2710\0\u2742\0\u2774"+ + "\0\u27a6\0\u27d8\0\u280a\0\u283c\0\u286e\0\u28a0\0\u28d2\0\u2904"+ + "\0\u2936\0\u2968\0\u299a\0\u29cc\0\u29fe\0\u2a30\0\u2a62\0\u2a94"+ + "\0\u2ac6\0\u2af8\0\u2b2a\0\u2b5c\0\u2b8e\0\u2bc0\0\u2bf2\0\u2c24"+ + "\0\u2c56\0\u2c88\0\u2cba\0\u2cec\0\u2d1e\0\u2d50\0\u2d82\0\u2db4"+ + "\0\u2de6\0\u2e18\0\u2e4a\0\u2e7c\0\u2eae\0\u2ee0\0\u2f12\0\u2f44"+ + "\0\u2f76\0\u2fa8\0\u2fda\0\u300c\0\u303e\0\u3070\0\u30a2\0\u30d4"+ + "\0\u3106\0\u3138\0\u316a\0\u319c\0\u31ce\0\u3200\0\u3232\0\u3264"+ + "\0\u3296\0\u32c8\0\u32fa\0\u332c\0\u335e\0\u3390\0\u33c2\0\u33f4"+ + "\0\u3426\0\u3458\0\u348a\0\u34bc\0\u34ee\0\u3520\0\u3552\0\u3584"+ + "\0\u35b6\0\u35e8\0\u361a\0\u364c\0\u367e\0\u36b0\0\u36e2\0\u3714"+ + "\0\u3746\0\u3778\0\u37aa\0\u37dc\0\u380e\0\u3840\0\u3872\0\u38a4"+ + "\0\u38d6\0\u3908\0\u393a\0\u396c\0\u399e\0\u39d0\0\u3a02\0\u3a34"+ + "\0\u3a66\0\u3a98\0\u3aca\0\u3afc\0\u3b2e\0\u3b60\0\u3b92\0\u3bc4"+ + "\0\u3bf6\0\u3c28\0\u3c5a\0\u3c8c\0\u3cbe\0\u3cf0\0\u3d22\0\u3d54"+ + "\0\u3d86\0\u3db8\0\u3dea\0\u3e1c\0\u3e4e\0\u3e80\0\u3eb2\0\u3ee4"+ + "\0\u3f16\0\u3f48\0\u3f7a\0\u3fac\0\u3fde\0\u4010\0\u4042\0\u4074"+ + "\0\u40a6\0\u40d8\0\u410a\0\u413c\0\u416e\0\u41a0\0\u41d2\0\372"+ + "\0\u4204\0\u4236\0\u4268\0\u429a\0\u42cc\0\u42fe\0\u4330\0\372"+ + "\0\u4362\0\u4394\0\u012c\0\u43c6\0\u43f8\0\u442a\0\u445c\0\u448e"+ + "\0\u44c0\0\u44f2\0\u4524\0\u4556\0\u4588\0\u45ba\0\u45ec\0\u461e"+ + "\0\u4650\0\u4682\0\u46b4\0\u46e6\0\u4718\0\u474a\0\u477c\0\u47ae"+ + "\0\u47e0\0\u4812\0\u4844\0\u4876\0\u48a8\0\u48da\0\u490c\0\u493e"+ + "\0\u4970\0\u49a2\0\u364c\0\u49d4\0\u4a06\0\u4a38\0\u4a6a\0\u4a9c"+ + "\0\u4ace\0\u4b00\0\u4b32\0\u4b64\0\u4b96\0\u4bc8\0\u4bfa\0\u4c2c"+ + "\0\u4c5e\0\u4c90\0\u4cc2\0\u4cf4\0\u4d26\0\u4d58\0\u4d8a\0\u4dbc"+ + "\0\u4dee\0\u4e20\0\u4e52\0\u4e84\0\u4eb6\0\u4ee8\0\u4f1a\0\u4f4c"+ + "\0\u4f7e\0\u4fb0\0\u4fe2\0\u5014\0\u5046\0\u5078\0\u50aa\0\u50dc"+ + "\0\u510e\0\u012c\0\u5140\0\u5172\0\u51a4\0\u51d6\0\u5208\0\u523a"+ + "\0\u526c\0\u529e\0\u52d0\0\u5302\0\u5334\0\u5366\0\u5398\0\u53ca"+ + "\0\u53fc\0\u542e\0\u5460\0\u5492\0\u54c4\0\u54f6\0\u5528\0\u555a"+ + "\0\u558c\0\u55be\0\u55f0\0\u5622\0\u5654\0\u5686\0\u56b8\0\u56ea"+ + "\0\u571c\0\u574e\0\u5780\0\u57b2\0\u57e4\0\u5816\0\u5848\0\u587a"+ + "\0\u58ac\0\u58de\0\u5910\0\u5942\0\u5974\0\u59a6\0\u59d8\0\u5a0a"+ + "\0\u5a3c\0\u5a6e\0\u5aa0\0\u5ad2\0\u5b04\0\u5b36\0\u5b68\0\u5b9a"+ + "\0\u5bcc\0\u5bfe\0\u5c30\0\u5c62\0\u5c94\0\u5cc6\0\u5cf8\0\u5d2a"+ + "\0\u5d5c\0\u5d8e\0\u5dc0\0\u5df2\0\u5e24\0\u5e56\0\u5e88\0\u5eba"+ + "\0\u5eec\0\u5f1e\0\u5f50\0\u5f82\0\u5fb4\0\u5fe6\0\u6018\0\u604a"+ + "\0\u607c\0\u60ae\0\u60e0\0\u6112\0\u6144\0\u6176\0\u61a8\0\u61da"+ + "\0\u620c\0\u623e\0\u6270\0\u62a2\0\u62d4\0\u6306\0\372\0\u6338"+ + "\0\u636a\0\u639c\0\u63ce\0\u6400\0\u6432\0\u6464\0\u6496\0\u64c8"+ + "\0\u64fa\0\u652c\0\u655e\0\u6590\0\u4c2c\0\u65c2\0\u65f4\0\u6626"+ + "\0\u6658\0\u668a\0\u66bc\0\u66ee\0\u6720\0\u6752\0\u6784\0\u67b6"+ + "\0\u67e8\0\u4f1a\0\372\0\u681a\0\u684c\0\u687e\0\u68b0\0\u68e2"+ + "\0\u6914\0\u6946\0\u6978\0\u69aa\0\u69dc\0\u6a0e\0\u6a40\0\u6a72"+ + "\0\u6aa4\0\u6ad6\0\u6b08\0\u6b3a\0\u6b6c\0\u6b9e\0\u6bd0\0\u6c02"+ + "\0\u6c34\0\u6c66\0\u6c98\0\u6cca\0\u6cfc\0\372\0\u6d2e\0\u6d60"+ + "\0\u6d92\0\u6dc4\0\u6df6\0\u6e28\0\u6e5a\0\u6e8c\0\u6ebe\0\u6ef0"+ + "\0\u6f22\0\u6f54\0\u6f86\0\u6fb8\0\u6fea\0\u701c\0\u704e\0\u7080"+ + "\0\u70b2\0\u70e4\0\u7116\0\u7148\0\u717a\0\u71ac\0\u71de\0\u7210"+ + "\0\u7242\0\u7274\0\u72a6\0\u72d8\0\u730a\0\u733c\0\u736e\0\u73a0"+ + "\0\u73d2\0\u7404\0\u7436\0\u7468\0\u749a\0\u74cc\0\u74fe\0\u7530"+ + "\0\u7562\0\u7594\0\u75c6\0\u75f8\0\u762a\0\u765c\0\u768e\0\u76c0"+ + "\0\u76f2\0\u7724\0\u7756\0\u7788\0\u77ba\0\u77ec\0\u781e\0\u7850"+ + "\0\u7882\0\u78b4\0\u78e6\0\u7918\0\u794a\0\u797c\0\u79ae\0\u79e0"+ + "\0\u7a12\0\u7a44\0\u7a76\0\u7aa8\0\u7ada\0\u7b0c\0\u7b3e\0\u7b70"+ + "\0\u7ba2\0\u7bd4\0\u7c06\0\u7c38\0\u7c6a\0\u7c9c\0\u7cce\0\u7d00"+ + "\0\u7d32\0\u7d64\0\u7d96\0\u7dc8\0\u7dfa\0\u7e2c\0\u7e5e\0\u7e90"+ + "\0\u7ec2\0\u7ef4\0\u7f26\0\u7f58\0\u7f8a\0\u7fbc\0\u7fee\0\u8020"+ + "\0\u8052\0\u8084\0\u80b6\0\u80e8\0\u811a\0\u814c\0\u817e\0\u81b0"+ + "\0\u81e2\0\u8214\0\u8246\0\u8278\0\u82aa\0\u82dc\0\u830e\0\u8340"+ + "\0\u8372\0\u83a4\0\u83d6\0\u8408\0\u843a\0\u846c\0\u849e\0\u84d0"+ + "\0\u8502\0\u8534\0\u8566\0\u8598\0\u85ca\0\u85fc\0\u862e\0\u8660"+ + "\0\u8692\0\u86c4\0\u86f6\0\u8728\0\u875a\0\u878c\0\u87be\0\u87f0"+ + "\0\u8822\0\u8854\0\u8886\0\u88b8\0\u88ea\0\u891c\0\u894e\0\u8980"+ + "\0\u89b2\0\u89e4\0\u8a16\0\u8a48\0\u8a7a\0\u8aac\0\u8ade\0\u8b10"+ + "\0\u8b42\0\u8b74\0\u8ba6\0\u8bd8\0\u8c0a\0\u8c3c\0\u8c6e\0\u8ca0"+ + "\0\u8cd2\0\u8d04\0\u8d36\0\u8d68\0\u8d9a\0\u8dcc\0\u8dfe\0\u8e30"+ + "\0\u8e62\0\u8e94\0\u8ec6\0\u8ef8\0\u8f2a\0\u8f5c\0\u8f8e\0\u8fc0"+ + "\0\u8ff2\0\u9024\0\u9056\0\u9088\0\u90ba\0\u90ec\0\u911e\0\u9150"+ + "\0\u9182\0\u91b4\0\u91e6\0\u9218\0\u924a\0\u927c\0\u92ae\0\u92e0"+ + "\0\u9312\0\u9344\0\u9376\0\u93a8\0\u93da\0\u940c\0\u943e\0\u9470"+ + "\0\u94a2\0\u94d4\0\u9506\0\u9538\0\u956a\0\u959c\0\u95ce\0\u9600"+ + "\0\u9632\0\u9664\0\u9696\0\u96c8\0\u96fa\0\u972c\0\u975e\0\u29cc"+ + "\0\u9790\0\u97c2\0\u97f4\0\u9826\0\u9858\0\u988a\0\u98bc\0\u98ee"+ + "\0\u9920\0\u9952\0\u9984\0\u99b6\0\u99e8\0\u9a1a\0\u9a4c\0\u9a7e"+ + "\0\u9ab0\0\u9ae2\0\u9b14\0\u9b46\0\u9b78\0\u9baa\0\u9bdc\0\u9c0e"+ + "\0\u9c40\0\u9c72\0\u9ca4\0\u9cd6\0\u9d08\0\u9d3a\0\u9d6c\0\u9d9e"+ + "\0\u9dd0\0\u9e02\0\u9e34\0\u9e66\0\u9e98\0\u9eca\0\u9efc\0\u9f2e"+ + "\0\u9f60\0\u9f92\0\u9fc4\0\u9ff6\0\ua028\0\ua05a\0\ua08c\0\ua0be"+ + "\0\ua0f0\0\ua122\0\ua154\0\ua186\0\ua1b8\0\ua1ea\0\ua21c\0\ua24e"+ + "\0\ua280\0\ua2b2\0\ua2e4\0\ua316\0\ua348\0\ua37a\0\ua3ac\0\ua3de"+ + "\0\ua410\0\ua442\0\ua474\0\ua4a6\0\ua4d8\0\ua50a\0\ua53c\0\ua56e"+ + "\0\ua5a0\0\ua5d2\0\ua604\0\ua636\0\ua668\0\ua69a\0\ua6cc\0\ua6fe"+ + "\0\ua730\0\ua762\0\ua794\0\ua7c6\0\ua7f8\0\ua82a\0\ua85c\0\ua88e"+ + "\0\ua8c0\0\ua8f2\0\ua924\0\ua956\0\ua988\0\ua9ba\0\ua9ec\0\uaa1e"+ + "\0\uaa50\0\uaa82\0\uaab4\0\uaae6\0\uab18\0\uab4a\0\uab7c\0\uabae"+ + "\0\uabe0\0\uac12\0\uac44\0\uac76\0\uaca8\0\uacda\0\uad0c\0\uad3e"+ + "\0\uad70\0\uada2\0\uadd4\0\uae06\0\uae38\0\uae6a\0\uae9c\0\uaece"+ + "\0\uaf00\0\uaf32\0\uaf64\0\uaf96\0\uafc8\0\uaffa\0\ub02c\0\ub05e"+ + "\0\ub090\0\ub0c2\0\ub0f4\0\ub126\0\ub158\0\ub18a\0\ub1bc\0\ub1ee"+ + "\0\ub220\0\ub252\0\ub284\0\ub2b6\0\ub2e8\0\ub31a\0\ub34c\0\ub37e"+ + "\0\ub3b0\0\ub3e2\0\ub414\0\ub446\0\ub478\0\ub4aa\0\ub4dc\0\ub50e"+ + "\0\ub540\0\ub572\0\ub5a4\0\ub5d6\0\ub608\0\ub63a\0\ub66c\0\ub69e"+ + "\0\ub6d0\0\ub702\0\ub734\0\u17a2\0\ub766\0\ub798\0\ub7ca\0\ub7fc"+ + "\0\ub82e\0\ub860\0\ub892\0\ub8c4\0\ub8f6\0\ub928\0\ub95a\0\ub98c"+ + "\0\ub9be\0\ub9f0\0\uba22\0\uba54\0\uba86\0\ubab8\0\ubaea\0\ubb1c"+ + "\0\ubb4e\0\ubb80\0\ubbb2\0\ubbe4\0\ubc16\0\ubc48\0\ubc7a\0\ubcac"+ + "\0\ubcde\0\ubd10\0\ubd42\0\ubd74\0\ubda6\0\ubdd8\0\ube0a\0\ube3c"+ + "\0\ube6e\0\ubea0\0\ubed2\0\ubf04\0\ubf36\0\ubf68\0\ubf9a\0\ubfcc"+ + "\0\ubffe\0\uc030\0\uc062\0\uc094\0\uc0c6\0\uc0f8\0\uc12a\0\uc15c"+ + "\0\uc18e\0\uc1c0\0\uc1f2\0\uc224\0\uc256\0\uc288\0\uc2ba\0\uc2ec"+ + "\0\uc31e\0\uc350\0\uc382\0\uc3b4\0\uc3e6\0\uc418\0\uc44a\0\uc47c"+ + "\0\uc4ae\0\uc4e0\0\uc512\0\uc544\0\uc576\0\uc5a8\0\uc5da\0\uc60c"+ + "\0\uc63e\0\uc670\0\uc6a2\0\uc6d4\0\uc706\0\uc738\0\uc76a\0\uc79c"+ + "\0\uc7ce\0\uc800\0\uc832\0\uc864\0\uc896\0\uc8c8\0\uc8fa\0\uc92c"+ + "\0\uc95e\0\uc990\0\uc9c2\0\uc9f4\0\uca26\0\uca58\0\uca8a\0\ucabc"+ + "\0\ucaee\0\ucb20\0\ucb52\0\ucb84\0\ucbb6\0\ucbe8\0\ucc1a\0\ucc4c"+ + "\0\ucc7e\0\uccb0\0\ucce2\0\ucd14\0\ucd46\0\ucd78\0\ucdaa\0\ucddc"+ + "\0\uce0e\0\uce40\0\uce72\0\ucea4\0\uced6\0\ucf08\0\ucf3a\0\ucf6c"+ + "\0\ucf9e\0\ucfd0\0\ud002\0\ud034\0\ud066\0\ud098\0\ud0ca\0\ud0fc"+ + "\0\ud12e\0\ud160\0\ud192\0\ud1c4\0\ud1f6\0\ud228\0\ud25a\0\ud28c"+ + "\0\ud2be\0\ud2f0\0\ud322\0\ud354\0\ud386\0\ud3b8\0\ud3ea\0\ud41c"+ + "\0\ud44e\0\ud480\0\ud4b2\0\ud4e4\0\ud516\0\ud548\0\ud57a\0\ud5ac"+ + "\0\ud5de\0\ud610\0\ud642\0\ud674\0\ud6a6\0\ud6d8\0\ud70a\0\ud73c"+ + "\0\ud76e\0\ud7a0\0\ud7d2\0\ud804\0\ud836\0\ud868\0\ud89a\0\ud8cc"+ + "\0\ud8fe\0\ud930\0\ud962\0\ud994\0\ud9c6\0\ud9f8\0\uda2a\0\uda5c"+ + "\0\uda8e\0\udac0\0\udaf2\0\udb24\0\udb56\0\udb88\0\udbba\0\udbec"+ + "\0\udc1e\0\udc50\0\udc82\0\udcb4\0\udce6\0\udd18\0\udd4a\0\udd7c"+ + "\0\uddae\0\udde0\0\ude12\0\ude44\0\ude76\0\udea8\0\udeda\0\udf0c"+ + "\0\udf3e\0\udf70\0\udfa2\0\udfd4\0\ue006\0\ue038\0\ue06a\0\ue09c"+ + "\0\ue0ce\0\ue100\0\ue132\0\ue164\0\ue196\0\ue1c8\0\ue1fa\0\ue22c"+ + "\0\ue25e\0\ue290\0\ue2c2\0\ue2f4\0\ue326\0\ue358\0\ue38a\0\ue3bc"+ + "\0\ue3ee\0\ue420\0\ue452\0\ue484\0\ue4b6\0\ue4e8\0\ue51a\0\ue54c"+ + "\0\ue57e\0\ue5b0\0\ue5e2\0\ue614\0\ue646\0\ue678\0\ue6aa\0\ue6dc"+ + "\0\ue70e\0\ue740\0\ue772\0\ue7a4\0\ue7d6\0\ue808\0\ue83a\0\ue86c"+ + "\0\ue89e\0\ue8d0\0\ue902\0\ue934\0\ue966\0\ue998\0\ue9ca\0\ue9fc"+ + "\0\uea2e\0\uea60\0\uea92\0\ueac4\0\ueaf6\0\ueb28\0\ueb5a\0\ueb8c"+ + "\0\uebbe\0\uebf0\0\uec22\0\uec54\0\uec86\0\uecb8\0\uecea\0\ued1c"+ + "\0\ued4e\0\ued80\0\uedb2\0\uede4\0\uee16\0\uee48\0\uee7a\0\ueeac"+ + "\0\ueede\0\uef10\0\uef42\0\uef74\0\uefa6\0\uefd8\0\uf00a\0\uf03c"+ + "\0\uf06e\0\uf0a0\0\uf0d2\0\uf104\0\uf136\0\uf168\0\uf19a\0\uf1cc"+ + "\0\uf1fe\0\uf230\0\uf262\0\uf294\0\uf2c6\0\uf2f8\0\uf32a\0\uf35c"+ + "\0\uf38e\0\uf3c0\0\uf3f2\0\uf424\0\uf456\0\uf488\0\uf4ba\0\uf4ec"+ + "\0\uf51e\0\uf550\0\uf582\0\uf5b4\0\uf5e6\0\uf618\0\uf64a\0\uf67c"+ + "\0\uf6ae\0\uf6e0\0\uf712\0\uf744\0\uf776\0\uf7a8\0\uf7da\0\uf80c"+ + "\0\uf83e\0\uf870\0\uf8a2\0\uf8d4\0\uf906\0\uf938\0\uf96a\0\uf99c"+ + "\0\uf9ce\0\ufa00\0\ufa32\0\ufa64\0\ufa96\0\ufac8\0\ufafa\0\ufb2c"+ + "\0\ufb5e\0\ufb90\0\ufbc2\0\ufbf4\0\ufc26\0\ufc58\0\ufc8a\0\ufcbc"+ + "\0\ufcee\0\ufd20\0\ufd52\0\ufd84\0\ufdb6\0\ufde8\0\ufe1a\0\ufe4c"+ + "\0\ufe7e\0\ufeb0\0\ufee2\0\uff14\0\uff46\0\uff78\0\uffaa\0\uffdc"+ + "\1\16\1\100\1\162\1\244\1\326\1\u0108\1\u013a\1\u016c"+ + "\1\u019e\1\u01d0\1\u0202\1\u0234\1\u0266\1\u0298\1\u02ca\1\u02fc"+ + "\1\u032e\1\u0360\1\u0392\1\u03c4\1\u03f6\1\u0428\1\u045a\1\u048c"+ + "\1\u04be\1\u04f0\1\u0522\1\u0554\1\u0586\1\u05b8\1\u05ea\1\u061c"+ + "\1\u064e\1\u0680\1\u06b2\1\u06e4\1\u0716\1\u0748\1\u077a\1\u07ac"+ + "\1\u07de\1\u0810\1\u0842\1\u0874\1\u08a6\1\u08d8\1\u090a\1\u093c"+ + "\1\u096e\1\u09a0\1\u09d2\1\u0a04\1\u0a36\1\u0a68\1\u0a9a\1\u0acc"+ + "\1\u0afe\1\u0b30\1\u0b62\1\u0b94\1\u0bc6\1\u0bf8\1\u0c2a\1\u0c5c"+ + "\1\u0c8e\1\u0cc0\1\u0cf2\1\u0d24\1\u0d56\1\u0d88\1\u0dba\1\u0dec"+ + "\1\u0e1e\1\u0e50\1\u0e82\1\u0eb4\1\u0ee6\1\u0f18\1\u0f4a\1\u0f7c"+ + "\1\u0fae\1\u0fe0\1\u1012\1\u1044\1\u1076\1\u10a8\1\u10da\1\u110c"+ + "\1\u113e\1\u1170\1\u11a2\1\u11d4\1\u1206\1\u1238\1\u126a\1\u129c"+ + "\1\u12ce\1\u1300\1\u1332\1\u1364\1\u1396\1\u13c8\1\u13fa\1\u142c"+ + "\1\u145e\1\u1490\1\u14c2\1\u14f4\1\u1526\1\u1558\1\u158a\1\u15bc"+ + "\1\u15ee\1\u1620\1\u1652\1\u1684\1\u16b6\1\u16e8\1\u171a\1\u174c"+ + "\1\u177e\1\u17b0\1\u17e2\1\u1814\1\u1846\1\u1878\1\u18aa\1\u18dc"+ + "\1\u190e\1\u1940\1\u1972\1\u19a4\1\u19d6\1\u1a08\1\u1a3a\1\u1a6c"+ + "\1\u1a9e\1\u1ad0\1\u1b02\1\u1b34\1\u1b66\1\u1b98\1\u1bca\1\u1bfc"+ + "\1\u1c2e\1\u1c60\1\u1c92\1\u1cc4\1\u1cf6\1\u1d28\1\u1d5a\1\u1d8c"+ + "\1\u1dbe\1\u1df0\1\u1e22\1\u1e54\1\u1e86\1\u1eb8\1\u1eea\1\u1f1c"+ + "\1\u1f4e\1\u1f80\1\u1fb2\1\u1fe4\1\u2016\1\u2048\1\u207a\1\u20ac"+ + "\1\u20de\1\u2110\1\u2142\1\u2174\1\u21a6\1\u21d8\1\u220a\1\u223c"+ + "\1\u226e\1\u22a0\1\u22d2\1\u2304\1\u2336\1\u2368\1\u239a\1\u23cc"+ + "\1\u23fe\1\u2430\1\u2462\1\u2494\1\u24c6\1\u24f8\1\u252a\1\u255c"+ + "\1\u258e\1\u25c0\1\u25f2\1\u2624\1\u2656\1\u2688\1\u26ba\1\u26ec"+ + "\1\u271e\1\u2750\1\u2782\1\u27b4\1\u27e6\1\u2818\1\u284a\1\u287c"+ + "\1\u28ae\1\u28e0\1\u2912\1\u2944\1\u2976\1\u29a8\1\u29da\1\u2a0c"+ + "\1\u2a3e\1\u2a70\1\u2aa2\1\u2ad4\1\u2b06\1\u2b38\1\u2b6a\1\u2b9c"+ + "\1\u2bce\1\u2c00\1\u2c32\1\u2c64\1\u2c96\1\u2cc8\1\u2cfa\1\u2d2c"+ + "\1\u2d5e\1\u2d90\1\u2dc2\1\u2df4\1\u2e26\1\u2e58\1\u2e8a\1\u2ebc"+ + "\1\u2eee\1\u2f20\1\u2f52\1\u2f84\1\u2fb6\1\u2fe8\1\u301a\1\u304c"+ + "\1\u307e\1\u30b0\1\u30e2\1\u3114\1\u3146\1\u3178\1\u31aa\1\u31dc"+ + "\1\u320e\1\u3240\1\u3272\1\u32a4\1\u32d6\1\u3308\1\u333a\1\u336c"+ + "\1\u339e\1\u33d0\1\u3402\1\u3434\1\u3466\1\u3498\1\u34ca\1\u34fc"+ + "\1\u352e\1\u3560\1\u3592\1\u35c4\1\u35f6\1\u3628\1\u365a\1\u368c"+ + "\1\u36be\1\u36f0\1\u3722\1\u3754\1\u3786\1\u37b8\1\u37ea\1\u381c"+ + "\1\u384e\1\u3880\1\u38b2\1\u38e4\1\u3916\1\u3948\1\u397a\1\u39ac"+ + "\1\u39de\1\u3a10\1\u3a42\1\u3a74\1\u3aa6\1\u3ad8\1\u3b0a\1\u3b3c"+ + "\1\u3b6e\1\u3ba0\1\u3bd2\1\u3c04\1\u3c36\1\u3c68\1\u3c9a\1\u3ccc"+ + "\1\u3cfe\1\u3d30\1\u3d62\1\u3d94\1\u3dc6\1\u3df8\1\u3e2a\1\u3e5c"+ + "\1\u3e8e\1\u3ec0\1\u3ef2\1\u3f24\1\u3f56\1\u3f88\1\u3fba\1\u3fec"+ + "\1\u401e\1\u4050\1\u4082\1\u40b4\1\u40e6\1\u4118\1\u414a\1\u417c"+ + "\1\u41ae\1\u41e0\1\u4212\1\u4244\1\u4276\1\u42a8\1\u42da\1\u430c"+ + "\1\u433e\1\u4370\1\u43a2\1\u43d4\1\u4406\1\u4438\1\u446a\1\u449c"+ + "\1\u44ce\1\u4500\1\u4532\1\u4564\1\u4596\1\u45c8\1\u45fa\1\u462c"+ + "\1\u465e\1\u4690\1\u46c2\1\u46f4\1\u4726\1\u4758\1\u478a\1\u47bc"+ + "\1\u47ee\1\u4820\1\u4852\1\u4884\1\u48b6\1\u48e8\1\u491a\1\u494c"+ + "\1\u497e\1\u49b0\1\u49e2\1\u4a14\1\u4a46\1\u4a78\1\u4aaa\1\u4adc"+ + "\1\u4b0e\1\u4b40\1\u4b72\1\u4ba4\1\u4bd6\1\u4c08\1\u4c3a\1\u4c6c"+ + "\1\u4c9e\1\u4cd0\1\u4d02\1\u4d34\1\u4d66\1\u4d98\1\u4dca\1\u4dfc"+ + "\1\u4e2e\1\u4e60\1\u4e92\1\u4ec4\1\u4ef6\1\u4f28\1\u4f5a\1\u4f8c"+ + "\1\u4fbe\1\u4ff0\1\u5022\1\u5054\1\u5086\1\u50b8\1\u50ea\1\u511c"+ + "\1\u514e\1\u5180\1\u51b2\1\u51e4\1\u5216\1\u5248\1\u527a\1\u52ac"+ + "\1\u52de\1\u5310\1\u5342\1\u5374\1\u53a6\1\u53d8\1\u540a\1\u543c"+ + "\1\u546e\1\u54a0\1\u54d2\1\u5504\1\u5536\1\u5568\1\u559a\1\u55cc"+ + "\1\u55fe\1\u5630\1\u5662\1\u5694\1\u56c6\1\u56f8\1\u572a\1\u575c"+ + "\1\u578e\1\u57c0\1\u57f2\1\u5824\1\u5856\1\u5888\1\u58ba\1\u58ec"+ + "\1\u591e\1\u5950\1\u5982\1\u59b4\1\u59e6\1\u5a18\1\u5a4a\1\u5a7c"+ + "\1\u5aae\1\u5ae0\1\u5b12\1\u5b44\1\u5b76\1\u5ba8\1\u5bda\1\u5c0c"+ + "\1\u5c3e\1\u5c70\1\u5ca2\1\u5cd4\1\u5d06\1\u5d38\1\u5d6a\1\u5d9c"+ + "\1\u5dce\1\u5e00\1\u5e32\1\u5e64\1\u5e96\1\u5ec8\1\u5efa\1\u5f2c"+ + "\1\u5f5e\1\u5f90\1\u5fc2\1\u5ff4\1\u6026\1\u6058\1\u608a\1\u60bc"+ + "\1\u60ee\1\u6120\1\u6152\1\u6184\1\u61b6\1\u61e8\1\u621a\1\u624c"+ + "\1\u627e\1\u62b0\1\u62e2\1\u6314\1\u6346\1\u6378\1\u63aa\1\u63dc"+ + "\1\u640e\1\u6440\1\u6472\1\u64a4\1\u64d6\1\u6508\1\u653a\1\u656c"+ + "\1\u659e\1\u65d0\1\u6602\1\u6634\1\u6666\1\u6698\1\u66ca\1\u66fc"+ + "\1\u672e\1\u6760\1\u6792\1\u67c4\1\u67f6\1\u6828\1\u685a\1\u688c"+ + "\1\u68be\1\u68f0\1\u6922\1\u6954\1\u6986\1\u69b8\1\u69ea\1\u6a1c"+ + "\1\u6a4e\1\u6a80\1\u6ab2\1\u6ae4\1\u6b16\1\u6b48\1\u6b7a\1\u6bac"+ + "\1\u6bde\1\u6c10\1\u6c42\1\u6c74\1\u6ca6\1\u6cd8\1\u6d0a\1\u6d3c"+ + "\1\u6d6e\1\u6da0\1\u6dd2\1\u6e04\1\u6e36\1\u6e68\1\u6e9a\1\u6ecc"+ + "\1\u6efe\1\u6f30\1\u6f62\1\u6f94\1\u6fc6\1\u6ff8\1\u702a\1\u705c"+ + "\1\u708e\1\u70c0\1\u70f2\1\u7124\1\u7156\1\u7188\1\u71ba\1\u71ec"+ + "\1\u721e\1\u7250\1\u7282\1\u72b4\1\u72e6\1\u7318\1\u734a\1\u737c"+ + "\1\u73ae\1\u73e0\1\u7412\1\u7444\1\u7476\1\u74a8\1\u74da\1\u750c"+ + "\1\u753e\1\u7570\1\u75a2\1\u75d4\1\u7606\1\u7638\1\u766a\1\u769c"+ + "\1\u76ce\1\u7700\1\u7732\1\u7764\1\u7796\1\u77c8\1\u77fa\1\u782c"+ + "\1\u785e\1\u7890\1\u78c2\1\u78f4\1\u7926\1\u7958\1\u798a\1\u79bc"+ + "\1\u79ee\1\u7a20\1\u7a52\1\u7a84\1\u7ab6\1\u7ae8\1\u7b1a\1\u7b4c"+ + "\1\u7b7e\1\u7bb0\1\u7be2\1\u7c14\1\u7c46\1\u7c78\1\u7caa\1\u7cdc"+ + "\1\u7d0e\1\u7d40\1\u7d72\1\u7da4\1\u7dd6\1\u7e08\1\u7e3a\1\u7e6c"+ + "\1\u7e9e\1\u7ed0\1\u7f02\1\u7f34\1\u7f66\1\u7f98\1\u7fca\1\u7ffc"+ + "\1\u802e\1\u8060\1\u8092\1\u80c4\1\u80f6\1\u8128\1\u815a\1\u818c"+ + "\1\u81be\1\u81f0\1\u8222\1\u8254\1\u8286\1\u82b8\1\u82ea\1\u831c"+ + "\1\u834e\1\u8380\1\u83b2\1\u83e4\1\u8416\1\u8448\1\u847a\1\u84ac"+ + "\1\u84de\1\u8510\1\u8542\1\u8574\1\u85a6\1\u85d8\1\u860a\1\u863c"+ + "\1\u866e\1\u86a0\1\u86d2\1\u8704\1\u8736\1\u8768\1\u879a\1\u87cc"+ + "\1\u87fe\1\u8830\1\u8862\1\u8894\1\u88c6\1\u88f8\1\u892a\1\u895c"+ + "\1\u898e\1\u89c0\1\u89f2\1\u8a24\1\u8a56\1\u8a88\1\u8aba\1\u8aec"+ + "\1\u8b1e\1\u8b50\1\u8b82\1\u8bb4\1\u8be6\1\u8c18\1\u8c4a\1\u8c7c"+ + "\1\u8cae\1\u8ce0\1\u8d12\1\u8d44\1\u8d76\1\u8da8\1\u8dda\1\u8e0c"+ + "\1\u8e3e\1\u8e70\1\u8ea2\1\u8ed4\1\u8f06\1\u8f38\1\u8f6a\1\u8f9c"+ + "\1\u8fce\1\u9000\1\u9032\1\u9064\1\u9096\1\u90c8\1\u90fa\1\u912c"+ + "\1\u915e\1\u9190\1\u91c2\1\u91f4\1\u9226\1\u9258\1\u928a\1\u92bc"+ + "\1\u92ee\1\u9320\1\u9352\1\u9384\1\u93b6\1\u93e8\1\u941a\1\u944c"+ + "\1\u947e\1\u94b0\1\u94e2\1\u9514\1\u9546\1\u9578\1\u95aa\1\u95dc"+ + "\1\u960e\1\u9640\1\u9672\1\u96a4\1\u96d6\1\u9708\1\u973a\1\u976c"+ + "\1\u979e\1\u97d0\1\u9802\1\u9834\1\u9866\1\u9898\1\u98ca\1\u98fc"+ + "\1\u992e\1\u9960\1\u9992\1\u99c4\1\u99f6\1\u9a28\1\u9a5a\1\u9a8c"+ + "\1\u9abe\1\u9af0\1\u9b22\1\u9b54\1\u9b86\1\u9bb8\1\u9bea\1\u9c1c"+ + "\1\u9c4e\1\u9c80\1\u9cb2\1\u9ce4\1\u9d16\1\u9d48\1\u9d7a\1\u9dac"+ + "\1\u9dde\1\u9e10\1\u9e42\1\u9e74\1\u9ea6\1\u9ed8\1\u9f0a\1\u9f3c"+ + "\1\u9f6e\1\u9fa0\1\u9fd2\1\ua004\1\ua036\1\ua068\1\ua09a\1\ua0cc"+ + "\1\ua0fe\1\ua130\1\ua162\1\ua194\1\ua1c6\1\ua1f8\1\ua22a\1\ua25c"+ + "\1\ua28e\1\ua2c0\1\ua2f2\1\ua324\1\ua356\1\ua388\1\ua3ba\1\ua3ec"+ + "\1\ua41e\1\ua450\1\ua482\1\ua4b4\1\ua4e6\1\ua518\1\ua54a\1\ua57c"+ + "\1\ua5ae\1\ua5e0\1\ua612\1\ua644\1\ua676\1\ua6a8\1\ua6da\1\ua70c"+ + "\1\ua73e\1\ua770\1\ua7a2\1\ua7d4\1\ua806\1\ua838\1\ua86a\1\ua89c"+ + "\1\ua8ce\1\ua900\1\ua932\1\ua964\1\ua996\1\ua9c8\1\ua9fa\1\uaa2c"+ + "\1\uaa5e\1\uaa90\1\uaac2\1\uaaf4\1\uab26\1\uab58\1\uab8a\1\uabbc"+ + "\1\uabee\1\uac20\1\uac52\1\uac84\1\uacb6\1\uace8\1\uad1a\1\uad4c"+ + "\1\uad7e\1\uadb0\1\uade2\1\uae14\1\uae46\1\uae78\1\uaeaa\1\uaedc"+ + "\1\uaf0e\1\uaf40\1\uaf72\1\uafa4\1\uafd6\1\ub008\1\ub03a\1\ub06c"+ + "\1\ub09e\1\ub0d0\1\ub102\1\ub134\1\ub166\1\ub198\1\ub1ca\1\ub1fc"+ + "\1\ub22e\1\ub260\1\ub292\1\ub2c4\1\ub2f6\1\ub328\1\ub35a\1\ub38c"+ + "\1\ub3be\1\ub3f0\1\ub422\1\ub454\1\ub486\1\ub4b8\1\ub4ea\1\ub51c"+ + "\1\ub54e\1\ub580\1\ub5b2\1\ub5e4\1\ub616\1\ub648\1\ub67a\1\ub6ac"+ + "\1\ub6de\1\ub710\1\ub742\1\ub774\1\ub7a6\1\ub7d8\1\ub80a\1\ub83c"+ + "\1\ub86e\1\ub8a0\1\ub8d2\1\ub904\1\ub936\1\ub968\1\ub99a\1\ub9cc"+ + "\1\ub9fe\1\uba30\1\uba62\1\uba94\1\ubac6\1\ubaf8\1\ubb2a\1\ubb5c"+ + "\1\ubb8e\1\ubbc0\1\ubbf2\1\ubc24\1\ubc56\1\ubc88\1\ubcba\1\ubcec"+ + "\1\ubd1e\1\ubd50\1\ubd82\1\ubdb4\1\ubde6\1\ube18\1\ube4a\1\ube7c"+ + "\1\ubeae\1\ubee0\1\ubf12\1\ubf44\1\ubf76\1\ubfa8\1\ubfda\1\uc00c"+ + "\1\uc03e\1\uc070\1\uc0a2\1\uc0d4\1\uc106\1\uc138\1\uc16a\1\uc19c"+ + "\1\uc1ce\1\uc200\1\uc232\1\uc264\1\uc296\1\uc2c8\1\uc2fa\1\uc32c"+ + "\1\uc35e\1\uc390\1\uc3c2\1\uc3f4\1\uc426\1\uc458\1\uc48a\1\uc4bc"+ + "\1\uc4ee\1\uc520\1\uc552\1\uc584\1\uc5b6\1\uc5e8\1\uc61a\1\uc64c"+ + "\1\uc67e\1\uc6b0\1\uc6e2\1\uc714\1\uc746\1\uc778\1\uc7aa\1\uc7dc"+ + "\1\uc80e\1\uc840\1\uc872\1\uc8a4\1\uc8d6\1\uc908\1\uc93a\1\uc96c"+ + "\1\uc99e\1\uc9d0\1\uca02\1\uca34\1\uca66\1\uca98\1\ucaca\1\ucafc"+ + "\1\ucb2e\1\ucb60\1\ucb92\1\ucbc4\1\ucbf6\1\ucc28\1\ucc5a\1\ucc8c"+ + "\1\uccbe\1\uccf0\1\ucd22\1\ucd54\1\ucd86\1\ucdb8\1\ucdea\1\uce1c"+ + "\1\uce4e\1\uce80\1\uceb2\1\ucee4\1\ucf16\1\ucf48\1\ucf7a\1\ucfac"+ + "\1\ucfde\1\ud010\1\ud042\1\ud074\1\ud0a6\1\ud0d8\1\ud10a\1\ud13c"+ + "\1\ud16e\1\ud1a0\1\ud1d2\1\ud204\1\ud236\1\ud268\1\ud29a\1\ud2cc"+ + "\1\ud2fe\1\ud330\1\ud362\1\ud394\1\ud3c6\1\ud3f8\1\ud42a\1\ud45c"+ + "\1\ud48e\1\ud4c0\1\ud4f2\1\ud524\1\ud556\1\ud588\1\ud5ba\1\ud5ec"+ + "\1\ud61e\1\ud650\1\ud682\1\ud6b4\1\ud6e6\1\ud718\1\ud74a\1\ud77c"+ + "\1\ud7ae\1\ud7e0\1\ud812\1\ud844\1\ud876\1\ud8a8\1\ud8da\1\ud90c"+ + "\1\ud93e\1\ud970\1\ud9a2\1\ud9d4\1\uda06\1\uda38\1\uda6a\1\uda9c"+ + "\1\udace\1\udb00\1\udb32\1\udb64\1\udb96\1\udbc8\1\udbfa\1\udc2c"+ + "\1\udc5e\1\udc90\1\udcc2\1\udcf4\1\udd26\1\udd58\1\udd8a\1\uddbc"+ + "\1\uddee\1\ude20\1\ude52\1\ude84\1\udeb6\1\udee8\1\udf1a\1\udf4c"+ + "\1\udf7e\1\udfb0\1\udfe2\1\ue014\1\ue046\1\ue078\1\ue0aa\1\ue0dc"+ + "\1\ue10e\1\ue140\1\ue172\1\ue1a4\1\ue1d6\1\ue208\1\ue23a\1\ue26c"+ + "\1\ue29e\1\ue2d0\1\ue302\1\ue334\1\ue366\1\ue398\1\ue3ca\1\ue3fc"+ + "\1\ue42e\1\ue460\1\ue492\1\ue4c4\1\ue4f6\1\ue528\1\ue55a\1\ue58c"+ + "\1\ue5be\1\ue5f0\1\ue622\1\ue654\1\ue686\1\ue6b8\1\ue6ea\1\ue71c"+ + "\1\ue74e\1\ue780\1\ue7b2\1\ue7e4\1\ue816\1\ue848\1\ue87a\1\ue8ac"+ + "\1\ue8de\1\ue910\1\ue942\1\ue974\1\ue9a6\1\ue9d8\1\uea0a\1\uea3c"+ + "\1\uea6e\1\ueaa0\1\uead2\1\ueb04\1\ueb36\1\ueb68\1\ueb9a\1\uebcc"+ + "\1\uebfe\1\uec30\1\uec62\1\uec94\1\uecc6\1\uecf8\1\ued2a\1\ued5c"+ + "\1\ued8e\1\uedc0\1\uedf2\1\uee24\1\uee56\1\uee88\1\ueeba\1\ueeec"+ + "\1\uef1e\1\uef50\1\uef82\1\uefb4\1\uefe6\1\uf018\1\uf04a\1\uf07c"+ + "\1\uf0ae\1\uf0e0\1\uf112\1\uf144\1\uf176\1\uf1a8\1\uf1da\1\uf20c"+ + "\1\uf23e\1\uf270\1\uf2a2\1\uf2d4\1\uf306\1\uf338\1\uf36a\1\uf39c"+ + "\1\uf3ce\1\uf400\1\uf432\1\uf464\1\uf496\1\uf4c8\1\uf4fa\1\uf52c"+ + "\1\uf55e\1\uf590\1\uf5c2\1\uf5f4\1\uf626\1\uf658\1\uf68a\1\uf6bc"+ + "\1\uf6ee\1\uf720\1\uf752\1\uf784\1\uf7b6\1\uf7e8\1\uf81a\1\uf84c"+ + "\1\uf87e\1\uf8b0\1\uf8e2\1\uf914\1\uf946\1\uf978\1\uf9aa\1\uf9dc"+ + "\1\ufa0e\1\ufa40\1\ufa72\1\ufaa4\1\ufad6\1\ufb08\1\ufb3a\1\ufb6c"+ + "\1\ufb9e\1\ufbd0\1\ufc02\1\ufc34\1\ufc66\1\ufc98\1\ufcca\1\ufcfc"+ + "\1\ufd2e\1\ufd60\1\ufd92\1\ufdc4\1\ufdf6\1\ufe28\1\ufe5a\1\ufe8c"+ + "\1\ufebe\1\ufef0\1\uff22\1\uff54\1\uff86\1\uffb8\1\uffea\2\34"+ + "\2\116\2\200\2\262\2\344\2\u0116\2\u0148\2\u017a\2\u01ac"+ + "\2\u01de\2\u0210\2\u0242\2\u0274\2\u02a6\2\u02d8\2\u030a\2\u033c"+ + "\2\u036e\2\u03a0\2\u03d2\2\u0404\2\u0436\2\u0468\2\u049a\2\u04cc"+ + "\2\u04fe\2\u0530\2\u0562\2\u0594\2\u05c6\2\u05f8\2\u062a\2\u065c"+ + "\2\u068e\2\u06c0\2\u06f2\2\u0724\2\u0756\2\u0788\2\u07ba\2\u07ec"+ + "\2\u081e\2\u0850\2\u0882\2\u08b4\2\u08e6\2\u0918\2\u094a\2\u097c"+ + "\2\u09ae\2\u09e0\2\u0a12\2\u0a44\2\u0a76\2\u0aa8\2\u0ada\2\u0b0c"+ + "\2\u0b3e\2\u0b70\2\u0ba2\2\u0bd4\2\u0c06\2\u0c38\2\u0c6a\2\u0c9c"+ + "\2\u0cce\2\u0d00\2\u0d32\2\u0d64\2\u0d96\2\u0dc8\2\u0dfa\2\u0e2c"+ + "\2\u0e5e\2\u0e90\2\u0ec2\2\u0ef4\2\u0f26\2\u0f58\2\u0f8a\2\u0fbc"+ + "\2\u0fee\2\u1020\2\u1052\2\u1084\2\u10b6\2\u10e8\2\u111a\2\u114c"+ + "\2\u117e\2\u11b0\2\u11e2\2\u1214\2\u1246\2\u1278\2\u12aa\2\u12dc"+ + "\2\u130e\2\u1340\2\u1372\2\u13a4\2\u13d6\2\u1408\2\u143a\2\u146c"+ + "\2\u149e\2\u14d0\2\u1502\2\u1534\2\u1566\2\u1598\2\u15ca\2\u15fc"+ + "\2\u162e\2\u1660\2\u1692\2\u16c4\2\u16f6\2\u1728\2\u175a\2\u178c"+ + "\2\u17be\2\u17f0\2\u1822\2\u1854\2\u1886\2\u18b8\2\u18ea\2\u191c"+ + "\2\u194e\2\u1980\2\u19b2\2\u19e4\2\u1a16\2\u1a48\2\u1a7a\2\u1aac"+ + "\2\u1ade\2\u1b10\2\u1b42\2\u1b74\2\u1ba6\2\u1bd8\2\u1c0a\2\u1c3c"+ + "\2\u1c6e\2\u1ca0"; + + private static int [] zzUnpackRowMap() { + int [] result = new int[2802]; + int offset = 0; + offset = zzUnpackRowMap(ZZ_ROWMAP_PACKED_0, offset, result); + return result; + } + + private static int zzUnpackRowMap(String packed, int offset, int [] result) { + int i = 0; /* index in packed string */ + int j = offset; /* index in unpacked array */ + int l = packed.length(); + while (i < l) { + int high = packed.charAt(i++) << 16; + result[j++] = high | packed.charAt(i++); + } + return j; + } + + /** + * The transition table of the DFA + */ + private static final int [] ZZ_TRANS = zzUnpackTrans(); + + private static final String ZZ_TRANS_PACKED_0 = + "\1\6\1\7\1\10\1\11\1\12\1\13\1\6\1\14"+ + "\1\15\1\16\1\17\1\20\1\21\1\22\1\23\1\24"+ + "\1\25\1\26\1\27\1\30\1\31\1\32\1\16\1\22"+ + "\1\33\1\34\2\35\1\36\1\37\1\40\1\41\1\42"+ + "\1\6\1\7\1\43\1\44\1\45\1\46\1\47\1\50"+ + "\1\51\1\52\1\53\1\7\1\54\1\7\1\55\1\56"+ + "\1\0\3\57\1\60\5\57\1\61\53\57\1\62\5\57"+ + "\1\63\50\57\3\64\1\65\3\64\1\66\52\64\3\67"+ + "\1\70\56\67\63\0\2\7\11\0\1\7\1\0\6\7"+ + "\1\0\1\7\6\0\3\7\1\0\1\7\1\0\1\7"+ + "\1\0\15\7\2\0\1\71\1\10\10\0\1\10\1\72"+ + "\1\0\6\71\1\0\1\71\6\0\3\71\1\0\1\71"+ + "\1\0\1\71\1\0\15\71\5\0\1\12\55\0\3\13"+ + "\1\0\1\13\1\73\1\74\53\13\10\0\1\75\63\0"+ + "\1\76\51\0\1\10\10\0\1\77\47\0\2\7\11\0"+ + "\1\7\1\0\1\7\1\100\3\7\1\101\1\102\1\103"+ + "\6\0\1\104\1\105\1\7\1\0\1\7\1\0\1\7"+ + "\1\0\6\7\1\106\6\7\2\0\2\7\11\0\1\107"+ + "\1\0\1\7\1\110\2\7\1\111\1\7\1\0\1\7"+ + "\6\0\2\7\1\112\1\0\1\7\1\0\1\7\1\113"+ + "\1\114\3\7\1\115\3\7\1\116\4\7\2\0\2\7"+ + "\11\0\1\117\1\0\4\7\1\120\1\7\1\0\1\7"+ + "\6\0\2\7\1\121\1\0\1\7\1\0\1\7\1\122"+ + "\1\123\14\7\2\0\2\7\11\0\1\7\1\0\6\7"+ + "\1\124\1\125\6\0\1\126\2\7\1\0\1\7\1\0"+ + "\1\7\1\0\3\7\1\127\11\7\2\0\2\7\11\0"+ + "\1\7\1\0\1\7\1\130\1\131\1\7\1\132\1\7"+ + "\1\0\1\7\6\0\2\7\1\133\1\0\1\7\1\0"+ + "\1\7\1\134\1\135\14\7\2\0\2\7\11\0\1\7"+ + "\1\0\1\7\1\136\3\7\1\137\1\140\1\141\6\0"+ + "\1\142\2\7\1\0\1\7\1\0\1\7\1\0\1\7"+ + "\1\143\13\7\2\0\2\7\11\0\1\144\1\0\2\7"+ + "\1\145\1\7\1\146\1\7\1\0\1\7\6\0\2\7"+ + "\1\147\1\0\1\7\1\0\1\7\1\150\1\151\14\7"+ + "\15\0\1\152\1\0\1\153\1\0\1\154\2\0\1\155"+ + "\12\0\1\156\2\0\2\157\2\160\1\161\1\0\1\162"+ + "\1\163\12\0\2\7\11\0\1\164\1\0\1\165\1\7"+ + "\1\166\2\7\1\167\1\0\1\7\6\0\2\7\1\170"+ + "\1\0\1\7\1\157\1\171\1\160\1\172\1\173\1\7"+ + "\1\174\1\175\10\7\15\0\1\176\7\0\2\177\4\0"+ + "\1\22\1\0\1\200\57\0\1\22\30\0\2\7\11\0"+ + "\1\201\1\0\2\7\1\202\1\7\1\203\1\7\1\0"+ + "\1\7\6\0\2\7\1\204\1\0\1\7\1\0\1\7"+ + "\1\205\1\206\14\7\2\0\2\7\11\0\1\7\1\0"+ + "\6\7\1\0\1\7\6\0\2\7\1\207\1\0\1\7"+ + "\1\0\1\7\1\0\10\7\1\210\4\7\2\0\2\7"+ + "\11\0\1\7\1\0\1\7\1\211\1\7\1\212\2\7"+ + "\1\0\1\7\6\0\1\213\2\7\1\0\1\7\1\0"+ + "\1\7\1\0\3\7\1\214\5\7\1\215\3\7\2\0"+ + "\2\7\11\0\1\7\1\0\1\7\1\216\1\217\1\7"+ + "\1\220\1\7\1\0\1\7\6\0\2\7\1\221\1\0"+ + "\1\7\1\0\1\7\1\222\1\223\14\7\17\0\1\224"+ + "\2\0\1\225\12\0\1\226\10\0\1\227\1\230\1\231"+ + "\13\0\2\7\11\0\1\7\1\0\1\232\2\7\1\233"+ + "\2\7\1\0\1\7\6\0\1\234\2\7\1\0\1\7"+ + "\1\0\1\7\1\0\1\7\1\235\1\236\1\237\11\7"+ + "\2\0\2\7\11\0\1\240\1\0\5\7\1\241\1\0"+ + "\1\7\6\0\2\7\1\242\1\0\1\7\1\0\1\7"+ + "\1\0\15\7\2\0\2\7\11\0\1\243\1\0\1\7"+ + "\1\244\3\7\1\245\1\0\1\7\6\0\2\7\1\246"+ + "\1\0\1\7\1\0\1\7\1\0\15\7\2\0\2\7"+ + "\11\0\1\7\1\0\1\7\1\247\1\250\1\7\1\251"+ + "\1\252\1\0\1\7\6\0\2\7\1\253\1\0\1\7"+ + "\1\0\1\7\1\0\1\7\1\254\13\7\2\0\2\7"+ + "\11\0\1\255\1\0\4\7\1\256\1\7\1\0\1\7"+ + "\6\0\3\7\1\0\1\7\1\0\1\7\1\257\1\260"+ + "\14\7\2\0\2\7\11\0\1\7\1\0\6\7\1\0"+ + "\1\7\6\0\2\7\1\261\1\0\1\7\1\0\1\7"+ + "\1\262\1\263\3\7\1\264\10\7\2\0\2\7\11\0"+ + "\1\7\1\0\6\7\1\0\1\7\6\0\3\7\1\0"+ + "\1\7\1\0\1\7\1\0\3\7\1\265\11\7\2\0"+ + "\2\7\11\0\1\266\1\0\4\7\1\267\1\7\1\0"+ + "\1\7\6\0\2\7\1\270\1\0\1\7\1\0\1\7"+ + "\1\0\15\7\2\0\2\7\11\0\1\7\1\0\4\7"+ + "\1\271\1\7\1\0\1\7\6\0\3\7\1\0\1\7"+ + "\1\0\1\7\1\0\15\7\2\0\2\7\11\0\1\7"+ + "\1\0\2\7\1\272\3\7\1\0\1\7\6\0\3\7"+ + "\1\0\1\7\1\0\1\7\1\0\15\7\2\0\2\7"+ + "\11\0\1\7\1\0\6\7\1\0\1\7\6\0\3\7"+ + "\1\0\1\7\1\0\1\7\1\0\2\7\1\273\6\7"+ + "\1\274\3\7\1\0\3\57\1\0\5\57\1\0\50\57"+ + "\11\0\1\275\61\0\1\276\50\0\3\64\1\0\3\64"+ + "\1\0\52\64\3\67\1\0\56\67\1\0\2\71\11\0"+ + "\1\71\1\0\6\71\1\0\1\71\6\0\3\71\1\0"+ + "\1\71\1\0\1\71\1\0\15\71\2\0\2\71\7\0"+ + "\1\71\1\0\10\71\1\0\1\71\6\0\3\71\1\0"+ + "\1\71\1\0\1\71\1\0\15\71\1\0\3\13\1\0"+ + "\1\13\1\277\1\74\53\13\10\0\1\300\64\0\1\22"+ + "\47\0\2\7\11\0\1\7\1\0\1\7\1\301\4\7"+ + "\1\0\1\7\6\0\3\7\1\0\1\7\1\0\1\7"+ + "\1\0\15\7\2\0\2\7\11\0\1\7\1\0\6\7"+ + "\1\302\1\303\6\0\3\7\1\0\1\7\1\0\1\7"+ + "\1\0\15\7\46\0\1\304\15\0\2\7\11\0\1\7"+ + "\1\0\6\7\1\0\1\7\6\0\3\7\1\0\1\7"+ + "\1\0\1\7\1\0\1\7\1\305\13\7\2\0\2\7"+ + "\11\0\1\7\1\0\4\7\1\306\1\7\1\0\1\7"+ + "\6\0\1\7\1\233\1\7\1\0\1\7\1\0\1\7"+ + "\1\0\1\7\1\307\13\7\2\0\2\7\11\0\1\7"+ + "\1\0\6\7\1\0\1\7\6\0\3\7\1\0\1\7"+ + "\1\0\1\7\1\310\1\311\14\7\2\0\2\7\11\0"+ + "\1\7\1\0\6\7\1\0\1\7\6\0\3\7\1\0"+ + "\1\7\1\0\1\7\1\0\1\7\1\312\13\7\2\0"+ + "\2\7\11\0\1\7\1\0\6\7\1\0\1\7\6\0"+ + "\3\7\1\0\1\7\1\0\1\7\1\0\6\7\1\311"+ + "\1\313\5\7\2\0\2\7\11\0\1\7\1\0\2\7"+ + "\1\314\1\7\1\315\1\7\1\0\1\7\6\0\3\7"+ + "\1\0\1\7\1\0\1\7\1\0\15\7\2\0\2\7"+ + "\11\0\1\7\1\0\6\7\1\0\1\7\6\0\3\7"+ + "\1\0\1\316\1\0\1\7\1\0\15\7\2\0\2\7"+ + "\11\0\1\7\1\0\6\7\1\317\1\320\6\0\1\321"+ + "\1\7\1\322\1\0\1\7\1\0\1\7\1\0\2\7"+ + "\1\323\1\324\11\7\35\0\1\310\26\0\2\7\11\0"+ + "\1\7\1\0\6\7\1\0\1\7\6\0\1\311\2\7"+ + "\1\0\1\7\1\0\1\7\1\0\15\7\2\0\2\7"+ + "\11\0\1\325\1\0\6\7\1\0\1\7\6\0\3\7"+ + "\1\0\1\7\1\0\1\7\1\0\15\7\2\0\2\7"+ + "\11\0\1\7\1\0\6\7\1\0\1\7\6\0\3\7"+ + "\1\0\1\7\1\0\1\7\1\0\3\7\1\326\11\7"+ + "\2\0\2\7\11\0\1\7\1\0\1\327\5\7\1\330"+ + "\1\331\6\0\3\7\1\0\1\7\1\0\1\7\1\0"+ + "\1\7\1\332\1\7\1\333\7\7\1\334\1\7\2\0"+ + "\2\7\11\0\1\7\1\0\6\7\1\0\1\7\6\0"+ + "\3\7\1\0\1\7\1\0\1\7\1\0\5\7\1\335"+ + "\7\7\2\0\2\7\11\0\1\7\1\0\6\7\1\0"+ + "\1\7\6\0\3\7\1\0\1\7\1\0\1\7\1\0"+ + "\5\7\1\336\7\7\47\0\1\337\14\0\2\7\11\0"+ + "\1\7\1\0\6\7\1\0\1\7\6\0\3\7\1\0"+ + "\1\7\1\0\1\7\1\0\2\7\1\340\12\7\15\0"+ + "\1\341\5\0\1\342\40\0\2\7\11\0\1\343\1\0"+ + "\4\7\1\344\1\7\1\0\1\7\6\0\3\7\1\0"+ + "\1\7\1\0\1\7\1\0\15\7\2\0\2\7\11\0"+ + "\1\7\1\0\1\345\5\7\1\0\1\7\6\0\3\7"+ + "\1\0\1\7\1\0\1\7\1\0\3\7\1\346\11\7"+ + "\2\0\2\7\11\0\1\7\1\0\6\7\1\0\1\7"+ + "\6\0\3\7\1\0\1\7\1\0\1\7\1\0\3\7"+ + "\1\347\11\7\2\0\2\7\11\0\1\7\1\0\4\7"+ + "\1\350\1\7\1\0\1\7\6\0\3\7\1\0\1\7"+ + "\1\0\1\7\1\0\15\7\2\0\2\7\11\0\1\7"+ + "\1\0\5\7\1\351\1\0\1\7\6\0\1\352\2\7"+ + "\1\0\1\7\1\0\1\7\1\0\15\7\2\0\2\7"+ + "\11\0\1\7\1\0\5\7\1\353\1\0\1\7\6\0"+ + "\3\7\1\0\1\7\1\0\1\7\1\0\15\7\2\0"+ + "\2\7\11\0\1\7\1\0\1\7\1\354\4\7\1\0"+ + "\1\7\6\0\1\311\2\7\1\0\1\7\1\0\1\7"+ + "\1\0\1\7\1\355\13\7\24\0\1\356\37\0\2\7"+ + "\11\0\1\7\1\0\5\7\1\351\1\0\1\7\6\0"+ + "\3\7\1\0\1\7\1\0\1\7\1\0\15\7\2\0"+ + "\2\7\11\0\1\357\1\0\6\7\1\0\1\7\6\0"+ + "\3\7\1\0\1\7\1\0\1\7\1\0\15\7\2\0"+ + "\2\7\11\0\1\7\1\0\6\7\1\0\1\7\6\0"+ + "\3\7\1\0\1\7\1\0\1\7\1\360\1\361\14\7"+ + "\25\0\2\362\35\0\2\7\11\0\1\7\1\0\6\7"+ + "\1\362\1\363\6\0\3\7\1\0\1\7\1\0\1\7"+ + "\1\0\15\7\2\0\2\7\11\0\1\7\1\0\6\7"+ + "\1\0\1\7\6\0\1\7\1\211\1\7\1\0\1\7"+ + "\1\0\1\7\1\0\15\7\2\0\2\7\11\0\1\7"+ + "\1\0\1\364\5\7\1\0\1\7\6\0\3\7\1\0"+ + "\1\7\1\0\1\7\1\0\15\7\2\0\2\7\11\0"+ + "\1\7\1\0\3\7\1\311\2\7\1\0\1\7\6\0"+ + "\3\7\1\0\1\7\1\0\1\7\1\0\15\7\2\0"+ + "\2\7\11\0\1\7\1\0\4\7\1\324\1\7\1\0"+ + "\1\7\6\0\3\7\1\0\1\7\1\0\1\7\1\0"+ + "\15\7\2\0\2\7\11\0\1\7\1\0\6\7\1\0"+ + "\1\7\6\0\3\7\1\0\1\365\1\0\1\7\1\0"+ + "\15\7\2\0\2\7\11\0\1\7\1\0\4\7\1\366"+ + "\1\7\1\0\1\7\6\0\2\7\1\367\1\0\1\7"+ + "\1\0\1\7\1\0\1\7\1\370\3\7\1\347\7\7"+ + "\25\0\2\310\35\0\2\7\11\0\1\7\1\0\6\7"+ + "\1\310\1\311\6\0\3\7\1\0\1\7\1\0\1\7"+ + "\1\0\15\7\17\0\1\371\4\0\1\372\23\0\1\373"+ + "\7\0\1\374\24\0\1\375\31\0\1\376\45\0\1\377"+ + "\57\0\1\u0100\4\0\2\u0101\35\0\1\u0102\104\0\2\u0103"+ + "\73\0\1\u0104\22\0\1\u0105\2\0\1\u0106\53\0\1\u0107"+ + "\67\0\1\u0108\40\0\2\7\11\0\1\7\1\0\1\u0109"+ + "\4\7\1\u010a\1\0\1\7\6\0\3\7\1\0\1\7"+ + "\1\0\1\7\1\0\3\7\1\u010b\7\7\1\u010c\1\7"+ + "\2\0\2\7\11\0\1\7\1\0\4\7\1\u010d\1\7"+ + "\1\0\1\7\6\0\3\7\1\0\1\7\1\0\1\7"+ + "\1\0\10\7\1\316\4\7\2\0\2\7\11\0\1\7"+ + "\1\0\6\7\1\0\1\7\6\0\3\7\1\0\1\u010e"+ + "\1\0\1\7\1\0\15\7\2\0\2\7\11\0\1\7"+ + "\1\0\6\7\1\0\1\7\6\0\2\7\1\u010f\1\0"+ + "\1\7\1\0\1\7\1\u0101\1\u0110\14\7\2\0\2\7"+ + "\11\0\1\7\1\0\2\7\1\u0111\3\7\1\0\1\7"+ + "\6\0\3\7\1\0\1\7\1\0\1\7\1\0\15\7"+ + "\2\0\2\7\11\0\1\7\1\0\6\7\1\0\1\7"+ + "\6\0\3\7\1\0\1\7\1\0\1\7\1\u0103\1\u0112"+ + "\14\7\2\0\2\7\11\0\1\7\1\0\6\7\1\0"+ + "\1\7\6\0\3\7\1\0\1\7\1\0\1\7\1\0"+ + "\12\7\1\u0113\2\7\2\0\2\7\11\0\1\7\1\0"+ + "\1\7\1\u0114\2\7\1\u0115\1\7\1\0\1\7\6\0"+ + "\3\7\1\0\1\7\1\0\1\7\1\0\15\7\2\0"+ + "\2\7\11\0\1\u0116\1\0\6\7\1\0\1\7\6\0"+ + "\3\7\1\0\1\7\1\0\1\7\1\0\15\7\2\0"+ + "\2\7\11\0\1\7\1\0\4\7\1\u0117\1\7\1\0"+ + "\1\7\6\0\3\7\1\0\1\7\1\0\1\7\1\0"+ + "\15\7\37\0\1\u0118\41\0\1\u0119\63\0\1\u011a\1\0"+ + "\1\u011b\40\0\2\7\11\0\1\7\1\0\6\7\1\0"+ + "\1\7\6\0\3\7\1\0\1\7\1\0\1\7\1\0"+ + "\6\7\1\u011c\6\7\2\0\2\7\11\0\1\7\1\0"+ + "\5\7\1\u011d\1\0\1\7\6\0\3\7\1\0\1\7"+ + "\1\0\1\7\1\0\15\7\2\0\2\7\11\0\1\7"+ + "\1\0\6\7\1\0\1\7\6\0\3\7\1\0\1\7"+ + "\1\0\1\7\1\0\7\7\1\u0113\5\7\2\0\2\7"+ + "\11\0\1\7\1\0\1\211\5\7\1\0\1\7\6\0"+ + "\3\7\1\0\1\u011e\1\0\1\7\1\0\15\7\24\0"+ + "\1\225\37\0\2\7\11\0\1\7\1\0\5\7\1\233"+ + "\1\0\1\7\6\0\3\7\1\0\1\7\1\0\1\7"+ + "\1\0\15\7\2\0\2\7\11\0\1\7\1\0\3\7"+ + "\1\u011f\2\7\1\0\1\7\6\0\3\7\1\0\1\7"+ + "\1\0\1\7\1\0\15\7\2\0\2\7\11\0\1\7"+ + "\1\0\6\7\1\0\1\7\6\0\1\u0120\2\7\1\0"+ + "\1\7\1\0\1\7\1\0\15\7\2\0\2\7\11\0"+ + "\1\7\1\0\3\7\1\u0121\2\7\1\0\1\7\6\0"+ + "\3\7\1\0\1\7\1\0\1\7\1\0\15\7\2\0"+ + "\2\7\11\0\1\7\1\0\6\7\1\0\1\7\6\0"+ + "\2\7\1\u0122\1\0\1\7\1\0\1\7\1\0\1\7"+ + "\1\u0123\13\7\2\0\2\7\11\0\1\7\1\0\4\7"+ + "\1\u0124\1\7\1\0\1\7\6\0\3\7\1\0\1\7"+ + "\1\0\1\7\1\0\15\7\2\0\2\7\11\0\1\u0125"+ + "\1\0\6\7\1\0\1\7\6\0\3\7\1\0\1\7"+ + "\1\0\1\7\1\0\15\7\2\0\2\7\11\0\1\u0126"+ + "\1\0\6\7\1\0\1\7\6\0\3\7\1\0\1\7"+ + "\1\0\1\7\1\0\15\7\2\0\2\7\11\0\1\7"+ + "\1\0\1\u0127\5\7\1\0\1\7\6\0\3\7\1\0"+ + "\1\7\1\0\1\7\1\0\15\7\2\0\2\7\11\0"+ + "\1\7\1\0\6\7\1\0\1\7\6\0\3\7\1\0"+ + "\1\7\1\0\1\7\1\0\1\7\1\u0128\5\7\1\324"+ + "\5\7\2\0\2\7\11\0\1\7\1\0\1\u0129\1\7"+ + "\1\u012a\3\7\1\0\1\7\6\0\3\7\1\0\1\7"+ + "\1\0\1\7\1\0\15\7\17\0\1\u012b\44\0\2\7"+ + "\11\0\1\7\1\0\1\u012c\5\7\1\0\1\7\6\0"+ + "\3\7\1\0\1\7\1\0\1\7\1\0\15\7\15\0"+ + "\1\u012d\63\0\1\u012e\2\0\1\u012f\13\0\1\u0130\62\0"+ + "\1\u0131\57\0\1\u0132\47\0\1\u0133\40\0\2\7\11\0"+ + "\1\u0134\1\0\6\7\1\0\1\7\6\0\3\7\1\0"+ + "\1\7\1\0\1\7\1\0\15\7\2\0\2\7\11\0"+ + "\1\7\1\0\1\u0135\2\7\1\u0136\2\7\1\0\1\7"+ + "\6\0\1\7\1\u0137\1\7\1\0\1\7\1\0\1\7"+ + "\1\0\15\7\2\0\2\7\11\0\1\7\1\0\6\7"+ + "\1\0\1\7\6\0\2\7\1\u0138\1\0\1\7\1\0"+ + "\1\7\1\0\15\7\2\0\2\7\11\0\1\7\1\0"+ + "\6\7\1\0\1\7\6\0\1\u0139\2\7\1\0\1\7"+ + "\1\0\1\7\1\0\15\7\2\0\2\7\11\0\1\7"+ + "\1\0\4\7\1\u013a\1\7\1\0\1\7\6\0\3\7"+ + "\1\0\1\7\1\0\1\7\1\0\15\7\2\0\2\7"+ + "\11\0\1\7\1\0\6\7\1\0\1\7\6\0\1\u013b"+ + "\2\7\1\0\1\7\1\0\1\7\1\0\15\7\2\0"+ + "\2\7\11\0\1\7\1\0\2\7\1\u013c\3\7\1\0"+ + "\1\7\6\0\3\7\1\0\1\7\1\0\1\7\1\u013d"+ + "\1\u013e\14\7\2\0\2\7\11\0\1\7\1\0\2\7"+ + "\1\114\2\7\1\u013f\1\0\1\7\6\0\3\7\1\0"+ + "\1\7\1\0\1\7\1\0\15\7\2\0\2\7\11\0"+ + "\1\7\1\0\1\u0140\5\7\1\0\1\7\6\0\3\7"+ + "\1\0\1\7\1\0\1\7\1\0\15\7\2\0\2\7"+ + "\11\0\1\u0141\1\0\6\7\1\0\1\7\6\0\3\7"+ + "\1\0\1\7\1\0\1\7\1\0\15\7\2\0\2\7"+ + "\11\0\1\7\1\0\6\7\1\0\1\7\6\0\2\7"+ + "\1\u0142\1\0\1\7\1\0\1\7\1\0\15\7\2\0"+ + "\2\7\11\0\1\7\1\0\1\u0143\5\7\1\0\1\7"+ + "\6\0\3\7\1\0\1\7\1\0\1\7\1\0\15\7"+ + "\2\0\2\7\11\0\1\7\1\0\6\7\1\0\1\7"+ + "\6\0\2\7\1\u0144\1\0\1\7\1\0\1\7\1\u0145"+ + "\1\u0146\14\7\2\0\2\7\11\0\1\7\1\0\5\7"+ + "\1\u0147\1\0\1\7\6\0\3\7\1\0\1\7\1\0"+ + "\1\7\1\0\15\7\2\0\2\7\11\0\1\7\1\0"+ + "\5\7\1\u0148\1\0\1\7\6\0\1\365\1\324\1\7"+ + "\1\0\1\7\1\0\1\7\1\u0149\1\u014a\1\7\1\u014b"+ + "\1\u014c\11\7\2\0\2\7\11\0\1\7\1\0\4\7"+ + "\1\u014d\1\7\1\0\1\7\6\0\3\7\1\0\1\7"+ + "\1\0\1\7\1\0\15\7\2\0\2\7\11\0\1\7"+ + "\1\0\1\7\1\u014e\4\7\1\u014f\1\u0150\6\0\3\7"+ + "\1\0\1\7\1\0\1\7\1\113\1\114\2\7\1\u0151"+ + "\11\7\2\0\2\7\11\0\1\7\1\0\4\7\1\u0152"+ + "\1\7\1\0\1\7\6\0\3\7\1\0\1\7\1\0"+ + "\1\7\1\0\15\7\2\0\2\7\11\0\1\7\1\0"+ + "\4\7\1\u0153\1\7\1\0\1\7\6\0\3\7\1\0"+ + "\1\7\1\0\1\7\1\122\1\123\14\7\2\0\2\7"+ + "\11\0\1\7\1\0\5\7\1\137\1\0\1\7\6\0"+ + "\1\u0154\2\7\1\0\1\7\1\0\1\7\1\0\15\7"+ + "\36\0\1\u0155\10\0\1\u0156\14\0\2\7\11\0\1\7"+ + "\1\0\6\7\1\0\1\7\6\0\1\7\1\u0157\1\7"+ + "\1\0\1\7\1\0\1\7\1\0\2\7\1\u0158\12\7"+ + "\2\0\2\7\11\0\1\7\1\0\1\7\1\u0159\4\7"+ + "\1\0\1\7\6\0\3\7\1\0\1\7\1\0\1\7"+ + "\1\0\15\7\36\0\1\u015a\25\0\2\7\11\0\1\7"+ + "\1\0\6\7\1\0\1\7\6\0\1\7\1\u015b\1\7"+ + "\1\0\1\7\1\0\1\7\1\0\15\7\2\0\2\7"+ + "\11\0\1\7\1\0\6\7\1\0\1\7\6\0\3\7"+ + "\1\0\1\7\1\0\1\7\1\u015c\1\u015d\14\7\2\0"+ + "\2\7\11\0\1\7\1\0\6\7\1\0\1\7\6\0"+ + "\3\7\1\0\1\7\1\0\1\7\1\0\1\7\1\254"+ + "\13\7\2\0\2\7\11\0\1\7\1\0\6\7\1\0"+ + "\1\7\6\0\1\u015e\2\7\1\0\1\7\1\0\1\7"+ + "\1\0\7\7\1\u015f\5\7\2\0\2\7\11\0\1\7"+ + "\1\0\6\7\1\0\1\7\6\0\3\7\1\0\1\7"+ + "\1\0\1\7\1\0\3\7\1\324\2\7\1\u0160\6\7"+ + "\2\0\2\7\11\0\1\7\1\0\6\7\1\u0161\1\u0162"+ + "\6\0\1\7\1\u0163\1\7\1\0\1\7\1\0\1\7"+ + "\1\0\11\7\1\u0164\3\7\2\0\2\7\11\0\1\7"+ + "\1\0\1\7\1\324\3\7\1\u0165\1\0\1\7\6\0"+ + "\3\7\1\0\1\7\1\0\1\7\1\0\15\7\2\0"+ + "\2\7\11\0\1\u0166\1\0\6\7\1\0\1\7\6\0"+ + "\3\7\1\0\1\7\1\0\1\7\1\0\15\7\2\0"+ + "\2\7\11\0\1\u0167\1\0\6\7\1\0\1\7\6\0"+ + "\3\7\1\0\1\7\1\0\1\7\1\0\15\7\11\0"+ + "\1\u0168\52\0\2\7\11\0\1\7\1\0\6\7\1\0"+ + "\1\7\6\0\2\7\1\u0169\1\0\1\7\1\0\1\7"+ + "\1\0\15\7\15\0\1\u016a\46\0\2\7\11\0\1\u016b"+ + "\1\0\6\7\1\0\1\7\6\0\3\7\1\0\1\7"+ + "\1\0\1\7\1\0\15\7\23\0\1\u016c\40\0\2\7"+ + "\11\0\1\7\1\0\4\7\1\u016d\1\7\1\0\1\7"+ + "\6\0\3\7\1\0\1\7\1\0\1\7\1\0\15\7"+ + "\2\0\2\7\11\0\1\7\1\0\6\7\1\0\1\7"+ + "\6\0\3\7\1\0\1\u016e\1\0\1\7\1\0\15\7"+ + "\2\0\2\7\11\0\1\7\1\0\2\7\1\u016f\3\7"+ + "\1\0\1\7\6\0\3\7\1\0\1\7\1\0\1\7"+ + "\1\0\15\7\17\0\1\u0170\44\0\2\7\11\0\1\7"+ + "\1\0\1\324\5\7\1\0\1\7\6\0\3\7\1\0"+ + "\1\7\1\0\1\7\1\0\15\7\2\0\2\7\11\0"+ + "\1\7\1\0\5\7\1\u0171\1\0\1\7\6\0\3\7"+ + "\1\0\1\7\1\0\1\7\1\0\15\7\2\0\2\7"+ + "\11\0\1\7\1\0\6\7\1\0\1\7\6\0\3\7"+ + "\1\0\1\7\1\0\1\7\1\0\3\7\1\u0172\11\7"+ + "\2\0\2\7\11\0\1\u0173\1\0\6\7\1\0\1\7"+ + "\6\0\3\7\1\0\1\7\1\0\1\7\1\0\15\7"+ + "\2\0\2\7\11\0\1\7\1\0\6\7\1\0\1\7"+ + "\6\0\1\u0174\2\7\1\0\1\7\1\0\1\7\1\0"+ + "\15\7\2\0\2\7\11\0\1\7\1\0\5\7\1\u0113"+ + "\1\0\1\7\6\0\3\7\1\0\1\7\1\0\1\7"+ + "\1\0\15\7\17\0\1\u0175\44\0\2\7\11\0\1\7"+ + "\1\0\1\u0176\5\7\1\0\1\7\6\0\3\7\1\0"+ + "\1\7\1\0\1\7\1\0\15\7\2\0\2\7\11\0"+ + "\1\7\1\0\2\7\1\u0177\3\7\1\0\1\7\6\0"+ + "\3\7\1\0\1\7\1\0\1\7\1\0\15\7\2\0"+ + "\2\7\11\0\1\7\1\0\5\7\1\u0178\1\0\1\7"+ + "\6\0\3\7\1\0\1\7\1\0\1\7\1\0\15\7"+ + "\2\0\2\7\11\0\1\7\1\0\6\7\1\0\1\7"+ + "\6\0\3\7\1\0\1\7\1\0\1\7\1\0\2\7"+ + "\1\316\12\7\2\0\2\7\11\0\1\7\1\0\6\7"+ + "\1\0\1\7\6\0\1\233\2\7\1\0\1\7\1\0"+ + "\1\7\1\0\15\7\2\0\2\7\11\0\1\273\1\0"+ + "\6\7\1\0\1\7\6\0\3\7\1\0\1\7\1\0"+ + "\1\7\1\0\15\7\2\0\2\7\11\0\1\7\1\0"+ + "\2\7\1\u0179\3\7\1\0\1\7\6\0\3\7\1\0"+ + "\1\7\1\0\1\7\1\0\15\7\50\0\1\u017a\13\0"+ + "\2\7\11\0\1\7\1\0\6\7\1\0\1\7\6\0"+ + "\3\7\1\0\1\7\1\0\1\7\1\0\3\7\1\u017b"+ + "\11\7\2\0\2\7\11\0\1\7\1\0\1\u017c\5\7"+ + "\1\0\1\7\6\0\3\7\1\0\1\7\1\0\1\7"+ + "\1\0\15\7\2\0\2\7\11\0\1\u017d\1\0\6\7"+ + "\1\0\1\7\6\0\3\7\1\0\1\7\1\0\1\7"+ + "\1\0\15\7\2\0\2\7\11\0\1\7\1\0\2\7"+ + "\1\u017e\3\7\1\0\1\7\6\0\3\7\1\0\1\7"+ + "\1\0\1\7\1\0\15\7\2\0\2\7\11\0\1\u017f"+ + "\1\0\6\7\1\u0180\1\u0181\6\0\3\7\1\0\1\7"+ + "\1\0\1\7\1\0\2\7\1\u0181\12\7\2\0\2\7"+ + "\11\0\1\7\1\0\6\7\1\0\1\7\6\0\3\7"+ + "\1\0\1\u0182\1\0\1\7\1\0\4\7\1\u0183\1\u0184"+ + "\7\7\51\0\1\310\12\0\2\7\11\0\1\7\1\0"+ + "\6\7\1\0\1\7\6\0\3\7\1\0\1\7\1\0"+ + "\1\7\1\0\4\7\1\311\10\7\22\0\1\u0185\1\u0186"+ + "\13\0\1\u0187\71\0\1\u0188\14\0\2\7\11\0\1\7"+ + "\1\0\3\7\1\u0189\1\u018a\1\7\1\0\1\7\6\0"+ + "\2\7\1\u018b\1\0\1\7\1\0\1\7\1\0\15\7"+ + "\2\0\2\7\11\0\1\7\1\0\6\7\1\0\1\7"+ + "\6\0\3\7\1\0\1\7\1\0\1\7\1\0\2\7"+ + "\1\u018c\12\7\2\0\2\7\11\0\1\7\1\0\6\7"+ + "\1\0\1\7\6\0\3\7\1\0\1\7\1\0\1\7"+ + "\1\205\1\206\14\7\2\0\2\7\11\0\1\7\1\0"+ + "\4\7\1\u018d\1\7\1\0\1\7\6\0\3\7\1\0"+ + "\1\7\1\0\1\7\1\0\15\7\2\0\2\7\11\0"+ + "\1\u018e\1\0\6\7\1\0\1\7\6\0\3\7\1\0"+ + "\1\7\1\0\1\7\1\0\15\7\2\0\2\7\11\0"+ + "\1\7\1\0\6\7\1\0\1\7\6\0\3\7\1\0"+ + "\1\7\1\0\1\7\1\0\7\7\1\u018f\5\7\2\0"+ + "\2\7\11\0\1\7\1\0\5\7\1\324\1\0\1\7"+ + "\6\0\3\7\1\0\1\7\1\0\1\7\1\0\15\7"+ + "\2\0\2\7\11\0\1\7\1\0\6\7\1\0\1\7"+ + "\6\0\3\7\1\0\1\7\1\0\1\7\1\0\1\7"+ + "\1\u0190\13\7\2\0\2\7\11\0\1\7\1\0\6\7"+ + "\1\u0191\1\314\6\0\3\7\1\0\1\7\1\0\1\7"+ + "\1\0\15\7\2\0\2\7\11\0\1\7\1\0\6\7"+ + "\1\0\1\7\6\0\3\7\1\0\1\7\1\0\1\7"+ + "\1\0\1\7\1\u0113\13\7\2\0\2\7\11\0\1\7"+ + "\1\0\2\7\1\u0192\3\7\1\0\1\7\6\0\3\7"+ + "\1\0\1\7\1\0\1\7\1\0\15\7\24\0\1\u0170"+ + "\37\0\2\7\11\0\1\7\1\0\4\7\1\u0193\1\7"+ + "\1\0\1\7\6\0\3\7\1\0\1\7\1\0\1\7"+ + "\1\0\15\7\47\0\1\u0131\14\0\2\7\11\0\1\7"+ + "\1\0\6\7\1\0\1\7\6\0\3\7\1\0\1\7"+ + "\1\0\1\7\1\0\2\7\1\u0138\12\7\15\0\1\u0194"+ + "\46\0\2\7\11\0\1\u0195\1\0\6\7\1\0\1\7"+ + "\6\0\3\7\1\0\1\7\1\0\1\7\1\0\15\7"+ + "\2\0\2\7\11\0\1\7\1\0\6\7\1\0\1\7"+ + "\6\0\3\7\1\0\1\7\1\0\1\7\1\u0196\1\u0197"+ + "\14\7\2\0\2\7\11\0\1\351\1\0\6\7\1\0"+ + "\1\7\6\0\3\7\1\0\1\7\1\0\1\7\1\0"+ + "\15\7\2\0\2\7\11\0\1\7\1\0\6\7\1\0"+ + "\1\7\6\0\1\7\1\u0198\1\7\1\0\1\7\1\0"+ + "\1\7\1\0\15\7\2\0\2\7\11\0\1\7\1\0"+ + "\6\7\1\0\1\7\6\0\3\7\1\0\1\7\1\0"+ + "\1\7\1\0\3\7\1\324\11\7\2\0\2\7\11\0"+ + "\1\7\1\0\4\7\1\206\1\7\1\0\1\7\6\0"+ + "\3\7\1\0\1\7\1\0\1\7\1\0\15\7\17\0"+ + "\1\u0199\2\0\1\u019a\31\0\1\u019b\22\0\1\u019c\61\0"+ + "\1\u019d\65\0\1\u019e\57\0\1\u0104\66\0\1\u0104\54\0"+ + "\1\u019f\61\0\1\u01a0\100\0\1\u01a1\60\0\1\u01a2\74\0"+ + "\1\u01a3\26\0\1\u0170\103\0\1\u01a4\46\0\1\u01a5\61\0"+ + "\1\u01a6\73\0\1\u01a7\25\0\2\7\11\0\1\7\1\0"+ + "\1\u01a8\2\7\1\u01a9\2\7\1\0\1\7\6\0\3\7"+ + "\1\0\1\7\1\0\1\7\1\0\7\7\1\u01aa\5\7"+ + "\2\0\2\7\11\0\1\u01ab\1\0\6\7\1\0\1\7"+ + "\6\0\3\7\1\0\1\7\1\0\1\7\1\0\15\7"+ + "\2\0\2\7\11\0\1\u01ac\1\0\6\7\1\0\1\7"+ + "\6\0\3\7\1\0\1\7\1\0\1\7\1\0\15\7"+ + "\2\0\2\7\11\0\1\7\1\0\2\7\1\u01ad\3\7"+ + "\1\0\1\7\6\0\3\7\1\0\1\7\1\0\1\7"+ + "\1\0\15\7\2\0\2\7\11\0\1\7\1\0\1\u0113"+ + "\5\7\1\0\1\7\6\0\3\7\1\0\1\7\1\0"+ + "\1\7\1\0\15\7\2\0\2\7\11\0\1\7\1\0"+ + "\1\u01ae\5\7\1\0\1\7\6\0\3\7\1\0\1\7"+ + "\1\0\1\7\1\0\15\7\2\0\2\7\11\0\1\7"+ + "\1\0\1\u01af\5\7\1\0\1\7\6\0\3\7\1\0"+ + "\1\7\1\0\1\7\1\0\15\7\2\0\2\7\11\0"+ + "\1\7\1\0\6\7\1\0\1\7\6\0\1\7\1\u01b0"+ + "\1\7\1\0\1\7\1\0\1\7\1\0\15\7\2\0"+ + "\2\7\11\0\1\7\1\0\6\7\1\0\1\7\6\0"+ + "\1\u01b1\2\7\1\0\1\7\1\0\1\7\1\0\15\7"+ + "\2\0\2\7\11\0\1\7\1\0\6\7\1\0\1\7"+ + "\6\0\3\7\1\0\1\7\1\0\1\7\1\0\3\7"+ + "\1\u01b2\11\7\2\0\2\7\11\0\1\324\1\0\6\7"+ + "\1\0\1\7\6\0\3\7\1\0\1\7\1\0\1\7"+ + "\1\0\15\7\2\0\2\7\11\0\1\7\1\0\6\7"+ + "\1\0\1\7\6\0\2\7\1\u01b3\1\0\1\7\1\0"+ + "\1\7\1\0\15\7\2\0\2\7\11\0\1\7\1\0"+ + "\5\7\1\u01b4\1\0\1\7\6\0\3\7\1\0\1\7"+ + "\1\0\1\7\1\0\15\7\2\0\2\7\11\0\1\7"+ + "\1\0\5\7\1\u01b5\1\0\1\7\6\0\3\7\1\0"+ + "\1\7\1\0\1\7\1\0\15\7\2\0\2\7\11\0"+ + "\1\7\1\0\6\7\1\0\1\7\6\0\1\7\1\245"+ + "\1\7\1\0\1\7\1\0\1\7\1\0\15\7\22\0"+ + "\1\u01b6\57\0\1\u01b7\115\0\1\u01b8\61\0\1\u01b9\7\0"+ + "\2\7\11\0\1\7\1\0\1\273\5\7\1\0\1\7"+ + "\6\0\3\7\1\0\1\7\1\0\1\7\1\0\15\7"+ + "\2\0\2\7\11\0\1\7\1\0\5\7\1\u01ba\1\0"+ + "\1\7\6\0\3\7\1\0\1\7\1\0\1\7\1\0"+ + "\15\7\2\0\2\7\11\0\1\7\1\0\6\7\1\0"+ + "\1\7\6\0\2\7\1\u01bb\1\0\1\7\1\0\1\7"+ + "\1\0\15\7\2\0\2\7\11\0\1\7\1\0\6\7"+ + "\1\0\1\7\6\0\3\7\1\0\1\7\1\0\1\7"+ + "\1\u01bc\1\u01bd\14\7\2\0\2\7\11\0\1\7\1\0"+ + "\4\7\1\u01be\1\7\1\0\1\7\6\0\3\7\1\0"+ + "\1\7\1\0\1\7\1\0\15\7\2\0\2\7\11\0"+ + "\1\7\1\0\6\7\1\u01bf\1\u01c0\6\0\3\7\1\0"+ + "\1\7\1\0\1\7\1\0\15\7\2\0\2\7\11\0"+ + "\1\7\1\0\6\7\1\0\1\7\6\0\3\7\1\0"+ + "\1\7\1\0\1\7\1\0\3\7\1\u01c1\11\7\2\0"+ + "\2\7\11\0\1\7\1\0\5\7\1\u01c2\1\0\1\7"+ + "\6\0\3\7\1\0\1\7\1\0\1\7\1\0\15\7"+ + "\2\0\2\7\11\0\1\7\1\0\6\7\1\0\1\7"+ + "\6\0\3\7\1\0\1\7\1\0\1\7\1\0\1\7"+ + "\1\u01c3\13\7\2\0\2\7\11\0\1\7\1\0\1\7"+ + "\1\u01c4\4\7\1\0\1\7\6\0\3\7\1\0\1\7"+ + "\1\0\1\7\1\0\15\7\2\0\2\7\11\0\1\7"+ + "\1\0\4\7\1\u01c5\1\7\1\0\1\7\6\0\3\7"+ + "\1\0\1\7\1\0\1\7\1\0\15\7\2\0\2\7"+ + "\11\0\1\7\1\0\1\235\5\7\1\0\1\7\6\0"+ + "\3\7\1\0\1\7\1\0\1\7\1\0\15\7\2\0"+ + "\2\7\11\0\1\7\1\0\6\7\1\0\1\7\6\0"+ + "\3\7\1\0\1\7\1\u01c6\1\u01c7\1\0\15\7\2\0"+ + "\2\7\11\0\1\7\1\0\1\u01c8\5\7\1\0\1\7"+ + "\6\0\3\7\1\0\1\7\1\0\1\7\1\0\15\7"+ + "\2\0\2\7\11\0\1\7\1\0\6\7\1\0\1\7"+ + "\6\0\1\u01c9\2\7\1\0\1\7\1\0\1\7\1\0"+ + "\15\7\54\0\1\u01ca\7\0\2\7\11\0\1\7\1\0"+ + "\6\7\1\0\1\7\6\0\3\7\1\0\1\7\1\0"+ + "\1\7\1\0\7\7\1\u01cb\5\7\54\0\1\u0170\22\0"+ + "\1\u01cc\110\0\2\u01cd\31\0\1\113\101\0\1\u0170\63\0"+ + "\1\u01ce\66\0\2\u0149\16\0\2\7\11\0\1\7\1\0"+ + "\6\7\1\0\1\7\6\0\3\7\1\0\1\7\1\0"+ + "\1\7\1\0\7\7\1\324\5\7\2\0\2\7\11\0"+ + "\1\u01cf\1\0\6\7\1\0\1\7\6\0\3\7\1\0"+ + "\1\7\1\0\1\7\1\0\15\7\2\0\2\7\11\0"+ + "\1\7\1\0\6\7\1\0\1\7\6\0\3\7\1\0"+ + "\1\7\1\0\1\7\1\u01cd\1\u01d0\14\7\2\0\2\7"+ + "\11\0\1\114\1\0\6\7\1\0\1\7\6\0\3\7"+ + "\1\0\1\7\1\0\1\7\1\0\15\7\2\0\2\7"+ + "\11\0\1\7\1\0\6\7\1\0\1\7\6\0\1\324"+ + "\2\7\1\0\1\7\1\0\1\7\1\0\15\7\2\0"+ + "\2\7\11\0\1\7\1\0\6\7\1\0\1\7\6\0"+ + "\2\7\1\u01d1\1\0\1\7\1\0\1\7\1\0\15\7"+ + "\2\0\2\7\11\0\1\7\1\0\6\7\1\0\1\7"+ + "\6\0\3\7\1\0\1\7\1\0\1\7\1\u0149\1\u014a"+ + "\14\7\2\0\2\7\11\0\1\7\1\0\1\347\5\7"+ + "\1\0\1\7\6\0\3\7\1\0\1\7\1\0\1\7"+ + "\1\0\15\7\2\0\2\7\11\0\1\7\1\0\6\7"+ + "\1\0\1\7\6\0\1\u01d2\2\7\1\0\1\7\1\0"+ + "\1\7\1\0\15\7\46\0\1\u01d3\15\0\2\7\11\0"+ + "\1\7\1\0\6\7\1\0\1\7\6\0\3\7\1\0"+ + "\1\7\1\0\1\7\1\0\1\7\1\u01d4\13\7\2\0"+ + "\2\7\11\0\1\7\1\0\2\7\1\u01d5\2\7\1\u01d6"+ + "\1\0\1\7\6\0\2\7\1\u01d7\1\0\1\7\1\0"+ + "\1\7\1\0\15\7\2\0\2\7\11\0\1\7\1\0"+ + "\3\7\1\u01a9\2\7\1\0\1\7\6\0\3\7\1\0"+ + "\1\7\1\0\1\7\1\0\7\7\1\u01aa\5\7\2\0"+ + "\2\7\11\0\1\7\1\0\6\7\1\0\1\7\6\0"+ + "\3\7\1\0\1\7\1\0\1\7\1\0\10\7\1\u01d8"+ + "\4\7\2\0\2\7\11\0\1\7\1\0\6\7\1\0"+ + "\1\7\6\0\3\7\1\0\1\7\1\0\1\7\1\0"+ + "\5\7\1\324\7\7\2\0\2\7\11\0\1\7\1\0"+ + "\6\7\1\0\1\7\6\0\2\7\1\233\1\0\1\7"+ + "\1\0\1\7\1\0\15\7\2\0\2\7\11\0\1\7"+ + "\1\0\6\7\1\0\1\7\6\0\3\7\1\0\1\7"+ + "\1\0\1\7\1\0\2\7\1\u01d9\12\7\35\0\1\u01da"+ + "\26\0\2\7\11\0\1\7\1\0\6\7\1\0\1\7"+ + "\6\0\1\u011c\2\7\1\0\1\7\1\0\1\7\1\0"+ + "\15\7\2\0\2\7\11\0\1\7\1\0\6\7\1\u0104"+ + "\1\u0113\6\0\3\7\1\0\1\7\1\0\1\7\1\0"+ + "\15\7\2\0\2\7\11\0\1\u01db\1\0\6\7\1\0"+ + "\1\7\6\0\3\7\1\0\1\7\1\0\1\7\1\0"+ + "\15\7\20\0\1\u01dc\43\0\2\7\11\0\1\7\1\0"+ + "\1\7\1\u01dd\4\7\1\0\1\7\6\0\3\7\1\0"+ + "\1\7\1\0\1\7\1\0\15\7\2\0\2\7\11\0"+ + "\1\u01de\1\0\6\7\1\0\1\7\6\0\3\7\1\0"+ + "\1\7\1\0\1\7\1\0\15\7\2\0\2\7\11\0"+ + "\1\u01df\1\0\6\7\1\0\1\7\6\0\3\7\1\0"+ + "\1\7\1\0\1\7\1\0\15\7\2\0\2\7\11\0"+ + "\1\7\1\0\6\7\1\0\1\7\6\0\3\7\1\0"+ + "\1\7\1\0\1\7\1\0\1\7\1\u01e0\13\7\2\0"+ + "\2\7\11\0\1\7\1\0\1\u01e1\5\7\1\0\1\7"+ + "\6\0\3\7\1\0\1\7\1\0\1\7\1\0\15\7"+ + "\44\0\2\u01e2\16\0\2\7\11\0\1\7\1\0\6\7"+ + "\1\0\1\7\6\0\3\7\1\0\1\7\1\0\1\7"+ + "\1\u01e2\1\u01e3\14\7\2\0\2\7\11\0\1\7\1\0"+ + "\2\7\1\u01e4\3\7\1\0\1\7\6\0\3\7\1\0"+ + "\1\7\1\0\1\7\1\0\15\7\2\0\2\7\11\0"+ + "\1\7\1\0\5\7\1\u01e5\1\0\1\7\6\0\3\7"+ + "\1\0\1\7\1\0\1\7\1\0\15\7\2\0\2\7"+ + "\11\0\1\7\1\0\5\7\1\u01e6\1\0\1\7\6\0"+ + "\3\7\1\0\1\7\1\0\1\7\1\0\15\7\2\0"+ + "\2\7\11\0\1\7\1\0\6\7\1\0\1\7\6\0"+ + "\1\7\1\316\1\7\1\0\1\7\1\0\1\7\1\0"+ + "\15\7\15\0\1\u01e7\115\0\1\u01e8\12\0\2\7\11\0"+ + "\1\u01e9\1\0\6\7\1\0\1\7\6\0\3\7\1\0"+ + "\1\7\1\0\1\7\1\0\15\7\2\0\2\7\11\0"+ + "\1\7\1\0\6\7\1\0\1\7\6\0\3\7\1\0"+ + "\1\7\1\0\1\7\1\0\4\7\1\u01ea\10\7\2\0"+ + "\2\7\11\0\1\7\1\0\5\7\1\u01eb\1\0\1\7"+ + "\6\0\3\7\1\0\1\7\1\0\1\7\1\0\15\7"+ + "\17\0\1\u01ec\44\0\2\7\11\0\1\7\1\0\1\u01ed"+ + "\5\7\1\0\1\7\6\0\3\7\1\0\1\7\1\0"+ + "\1\7\1\0\15\7\24\0\1\u01ee\37\0\2\7\11\0"+ + "\1\7\1\0\5\7\1\u01ef\1\0\1\7\6\0\3\7"+ + "\1\0\1\7\1\0\1\7\1\0\15\7\2\0\2\7"+ + "\11\0\1\7\1\0\2\7\1\324\3\7\1\0\1\7"+ + "\6\0\3\7\1\0\1\7\1\0\1\7\1\0\15\7"+ + "\2\0\2\7\11\0\1\7\1\0\6\7\1\0\1\7"+ + "\6\0\2\7\1\u01f0\1\0\1\7\1\0\1\7\1\0"+ + "\15\7\2\0\2\7\11\0\1\7\1\0\5\7\1\u01f1"+ + "\1\0\1\7\6\0\3\7\1\0\1\7\1\0\1\7"+ + "\1\0\1\7\1\u01f2\13\7\23\0\1\u01f3\40\0\2\7"+ + "\11\0\1\7\1\0\4\7\1\u01f4\1\7\1\0\1\7"+ + "\6\0\3\7\1\0\1\7\1\0\1\7\1\0\15\7"+ + "\2\0\2\7\11\0\1\7\1\0\2\7\1\u01bd\1\7"+ + "\1\351\1\7\1\0\1\7\6\0\3\7\1\0\1\7"+ + "\1\0\1\7\1\0\15\7\2\0\2\7\11\0\1\7"+ + "\1\0\6\7\1\0\1\7\6\0\3\7\1\0\1\7"+ + "\1\0\1\7\1\u0104\1\u0113\14\7\2\0\2\7\11\0"+ + "\1\7\1\0\6\7\1\0\1\7\6\0\3\7\1\0"+ + "\1\7\1\0\1\7\1\u01f5\1\u01f6\14\7\2\0\2\7"+ + "\11\0\1\7\1\0\2\7\1\u01f7\3\7\1\0\1\7"+ + "\6\0\3\7\1\0\1\7\1\0\1\7\1\0\15\7"+ + "\2\0\2\7\11\0\1\7\1\0\1\7\1\u01f8\4\7"+ + "\1\0\1\7\6\0\3\7\1\0\1\7\1\0\1\7"+ + "\1\0\15\7\2\0\2\7\11\0\1\7\1\0\1\7"+ + "\1\273\4\7\1\0\1\7\6\0\3\7\1\0\1\7"+ + "\1\0\1\7\1\0\15\7\44\0\2\u01f9\16\0\2\7"+ + "\11\0\1\7\1\0\6\7\1\0\1\7\6\0\3\7"+ + "\1\0\1\7\1\0\1\7\1\u01f9\1\u01fa\14\7\50\0"+ + "\1\u0104\13\0\2\7\11\0\1\7\1\0\6\7\1\0"+ + "\1\7\6\0\3\7\1\0\1\7\1\0\1\7\1\0"+ + "\3\7\1\u0113\11\7\2\0\2\7\11\0\1\7\1\0"+ + "\5\7\1\u01fb\1\0\1\7\6\0\3\7\1\0\1\7"+ + "\1\0\1\7\1\0\15\7\2\0\2\7\11\0\1\7"+ + "\1\0\6\7\1\0\1\7\6\0\3\7\1\0\1\7"+ + "\1\0\1\7\1\0\7\7\1\u01fc\5\7\2\0\2\7"+ + "\11\0\1\7\1\0\2\7\1\u01fd\3\7\1\0\1\7"+ + "\6\0\3\7\1\0\1\7\1\0\1\7\1\0\15\7"+ + "\2\0\2\7\11\0\1\7\1\0\5\7\1\u01fe\1\0"+ + "\1\7\6\0\3\7\1\0\1\7\1\0\1\7\1\0"+ + "\15\7\2\0\2\7\11\0\1\7\1\0\6\7\1\u01ff"+ + "\1\u0200\6\0\3\7\1\0\1\7\1\0\1\7\1\0"+ + "\15\7\20\0\1\u0201\43\0\2\7\11\0\1\7\1\0"+ + "\1\7\1\u0202\4\7\1\0\1\7\6\0\3\7\1\0"+ + "\1\7\1\0\1\7\1\0\15\7\2\0\2\7\11\0"+ + "\1\7\1\0\6\7\1\0\1\7\6\0\3\7\1\0"+ + "\1\7\1\0\1\7\1\0\7\7\1\u0203\5\7\2\0"+ + "\2\7\11\0\1\7\1\0\1\u0204\5\7\1\0\1\7"+ + "\6\0\3\7\1\0\1\7\1\0\1\7\1\0\15\7"+ + "\2\0\2\7\11\0\1\7\1\0\1\7\1\325\4\7"+ + "\1\0\1\7\6\0\3\7\1\0\1\7\1\0\1\7"+ + "\1\0\15\7\15\0\1\u0205\46\0\2\7\11\0\1\u0206"+ + "\1\0\6\7\1\0\1\7\6\0\3\7\1\0\1\7"+ + "\1\0\1\7\1\0\15\7\2\0\2\7\11\0\1\7"+ + "\1\0\4\7\1\u0207\1\7\1\0\1\7\6\0\3\7"+ + "\1\0\1\7\1\0\1\7\1\0\15\7\2\0\2\7"+ + "\11\0\1\7\1\0\4\7\1\u0208\1\7\1\0\1\7"+ + "\6\0\3\7\1\0\1\7\1\0\1\7\1\0\15\7"+ + "\2\0\2\7\11\0\1\7\1\0\6\7\1\0\1\7"+ + "\6\0\3\7\1\0\1\7\1\0\1\7\1\u0209\1\u020a"+ + "\14\7\2\0\2\7\11\0\1\7\1\0\6\7\1\0"+ + "\1\7\6\0\3\7\1\0\1\7\1\0\1\7\1\0"+ + "\13\7\1\u020b\1\7\15\0\1\u01da\46\0\2\7\11\0"+ + "\1\u011c\1\0\6\7\1\0\1\7\6\0\3\7\1\0"+ + "\1\7\1\0\1\7\1\0\15\7\2\0\2\7\11\0"+ + "\1\7\1\0\4\7\1\u020c\1\7\1\0\1\7\6\0"+ + "\3\7\1\0\1\7\1\0\1\7\1\0\15\7\2\0"+ + "\2\7\11\0\1\u020d\1\0\6\7\1\0\1\7\6\0"+ + "\3\7\1\0\1\7\1\0\1\7\1\0\15\7\2\0"+ + "\2\7\11\0\1\7\1\0\6\7\1\0\1\7\6\0"+ + "\3\7\1\0\1\7\1\0\1\7\1\262\1\263\14\7"+ + "\37\0\1\u020e\46\0\1\u020f\113\0\1\u0210\20\0\1\u0211"+ + "\46\0\2\7\11\0\1\7\1\0\6\7\1\0\1\7"+ + "\6\0\2\7\1\u0212\1\0\1\7\1\0\1\7\1\0"+ + "\15\7\2\0\2\7\11\0\1\7\1\0\5\7\1\u0213"+ + "\1\0\1\7\6\0\3\7\1\0\1\7\1\0\1\7"+ + "\1\0\15\7\2\0\2\7\11\0\1\7\1\0\6\7"+ + "\1\0\1\7\6\0\3\7\1\0\1\7\1\0\1\7"+ + "\1\0\11\7\1\215\3\7\2\0\2\7\11\0\1\u0192"+ + "\1\0\6\7\1\0\1\7\6\0\3\7\1\0\1\7"+ + "\1\0\1\7\1\0\15\7\2\0\2\7\11\0\1\7"+ + "\1\0\6\7\1\0\1\7\6\0\3\7\1\0\1\7"+ + "\1\0\1\7\1\0\1\7\1\u0214\13\7\2\0\2\7"+ + "\11\0\1\7\1\0\1\7\1\324\4\7\1\0\1\7"+ + "\6\0\3\7\1\0\1\7\1\0\1\7\1\0\15\7"+ + "\2\0\2\7\11\0\1\u0215\1\0\6\7\1\0\1\7"+ + "\6\0\3\7\1\0\1\7\1\0\1\7\1\0\15\7"+ + "\2\0\2\7\11\0\1\7\1\0\1\u0216\5\7\1\0"+ + "\1\7\6\0\3\7\1\0\1\7\1\0\1\7\1\0"+ + "\15\7\15\0\1\u0217\46\0\2\7\11\0\1\7\1\0"+ + "\6\7\1\u0170\1\324\6\0\3\7\1\0\1\7\1\0"+ + "\1\7\1\0\15\7\20\0\1\u01da\43\0\2\7\11\0"+ + "\1\7\1\0\1\7\1\u011c\4\7\1\0\1\7\6\0"+ + "\3\7\1\0\1\7\1\0\1\7\1\0\15\7\37\0"+ + "\1\u0218\24\0\2\7\11\0\1\7\1\0\6\7\1\0"+ + "\1\7\6\0\2\7\1\u0219\1\0\1\7\1\0\1\7"+ + "\1\0\15\7\2\0\2\7\11\0\1\7\1\0\3\7"+ + "\1\u011f\2\7\1\317\1\320\6\0\3\7\1\0\1\7"+ + "\1\0\1\7\1\0\15\7\44\0\2\u021a\31\0\1\u021b"+ + "\61\0\1\u021c\112\0\1\u01da\33\0\1\u021d\56\0\1\u021e"+ + "\110\0\2\u021f\60\0\2\u0220\31\0\1\u0221\102\0\1\u0170"+ + "\43\0\1\u0222\65\0\1\u0223\52\0\1\u0224\70\0\1\u0225"+ + "\74\0\1\u0226\24\0\2\7\11\0\1\7\1\0\6\7"+ + "\1\0\1\7\6\0\3\7\1\0\1\7\1\0\1\7"+ + "\1\u021a\1\u0227\14\7\2\0\2\7\11\0\1\u0228\1\0"+ + "\6\7\1\0\1\7\6\0\3\7\1\0\1\7\1\0"+ + "\1\7\1\0\15\7\2\0\2\7\11\0\1\u0229\1\0"+ + "\6\7\1\0\1\7\6\0\3\7\1\0\1\7\1\0"+ + "\1\7\1\0\15\7\2\0\2\7\11\0\1\7\1\0"+ + "\6\7\1\0\1\7\6\0\3\7\1\0\1\7\1\0"+ + "\1\7\1\0\1\7\1\u011c\13\7\2\0\2\7\11\0"+ + "\1\7\1\0\1\7\1\u022a\4\7\1\0\1\7\6\0"+ + "\3\7\1\0\1\7\1\0\1\7\1\0\15\7\2\0"+ + "\2\7\11\0\1\u022b\1\0\6\7\1\0\1\7\6\0"+ + "\3\7\1\0\1\7\1\0\1\7\1\0\15\7\2\0"+ + "\2\7\11\0\1\7\1\0\6\7\1\0\1\7\6\0"+ + "\3\7\1\0\1\7\1\0\1\7\1\u021f\1\u022c\14\7"+ + "\2\0\2\7\11\0\1\7\1\0\6\7\1\0\1\7"+ + "\6\0\3\7\1\0\1\7\1\0\1\7\1\u0220\1\u022d"+ + "\14\7\2\0\2\7\11\0\1\u022e\1\0\6\7\1\0"+ + "\1\7\6\0\3\7\1\0\1\7\1\0\1\7\1\0"+ + "\15\7\2\0\2\7\11\0\1\7\1\0\6\7\1\0"+ + "\1\7\6\0\1\7\1\324\1\7\1\0\1\7\1\0"+ + "\1\7\1\0\15\7\2\0\2\7\11\0\1\7\1\0"+ + "\1\7\1\u022f\4\7\1\0\1\7\6\0\3\7\1\0"+ + "\1\7\1\0\1\7\1\0\15\7\2\0\2\7\11\0"+ + "\1\7\1\0\5\7\1\u0230\1\0\1\7\6\0\3\7"+ + "\1\0\1\7\1\0\1\7\1\0\15\7\2\0\2\7"+ + "\11\0\1\u0231\1\0\6\7\1\0\1\7\6\0\3\7"+ + "\1\0\1\7\1\0\1\7\1\0\15\7\2\0\2\7"+ + "\11\0\1\7\1\0\5\7\1\u0232\1\0\1\7\6\0"+ + "\3\7\1\0\1\7\1\0\1\7\1\0\15\7\32\0"+ + "\1\u0233\73\0\2\u0234\55\0\1\u0235\35\0\1\u01b6\46\0"+ + "\2\7\11\0\1\7\1\0\1\7\1\u0236\4\7\1\0"+ + "\1\7\6\0\3\7\1\0\1\7\1\0\1\7\1\0"+ + "\15\7\24\0\1\u0237\37\0\2\7\11\0\1\7\1\0"+ + "\5\7\1\326\1\0\1\7\6\0\3\7\1\0\1\7"+ + "\1\0\1\7\1\0\15\7\2\0\2\7\11\0\1\7"+ + "\1\0\6\7\1\0\1\7\6\0\3\7\1\0\1\7"+ + "\1\0\1\7\1\0\7\7\1\u01f4\5\7\15\0\1\310"+ + "\46\0\2\7\11\0\1\311\1\0\6\7\1\0\1\7"+ + "\6\0\3\7\1\0\1\7\1\0\1\7\1\0\15\7"+ + "\2\0\2\7\11\0\1\u0138\1\0\6\7\1\0\1\7"+ + "\6\0\3\7\1\0\1\7\1\0\1\7\1\0\15\7"+ + "\2\0\2\7\11\0\1\7\1\0\6\7\1\0\1\7"+ + "\6\0\2\7\1\u0147\1\0\1\7\1\0\1\7\1\0"+ + "\15\7\2\0\2\7\11\0\1\7\1\0\6\7\1\0"+ + "\1\7\6\0\3\7\1\0\1\7\1\0\1\7\1\u0238"+ + "\1\u0239\14\7\2\0\2\7\11\0\1\7\1\0\5\7"+ + "\1\u023a\1\0\1\7\6\0\3\7\1\0\1\7\1\0"+ + "\1\7\1\0\15\7\2\0\2\7\11\0\1\7\1\0"+ + "\6\7\1\0\1\7\6\0\3\7\1\0\1\7\1\225"+ + "\1\233\1\0\15\7\47\0\1\u023b\14\0\2\7\11\0"+ + "\1\7\1\0\6\7\1\0\1\7\6\0\3\7\1\0"+ + "\1\7\1\0\1\7\1\0\2\7\1\u023c\12\7\2\0"+ + "\2\7\11\0\1\7\1\0\6\7\1\0\1\7\6\0"+ + "\2\7\1\u0134\1\0\1\7\1\0\1\7\1\0\15\7"+ + "\2\0\2\7\11\0\1\7\1\0\6\7\1\0\1\7"+ + "\6\0\1\7\1\u023d\1\7\1\0\1\7\1\0\1\7"+ + "\1\0\15\7\23\0\1\u023e\40\0\2\7\11\0\1\7"+ + "\1\0\4\7\1\367\1\7\1\0\1\7\6\0\3\7"+ + "\1\0\1\7\1\0\1\7\1\0\15\7\20\0\1\u023f"+ + "\76\0\1\u0240\44\0\1\u0241\43\0\2\7\11\0\1\7"+ + "\1\0\1\7\1\u0242\4\7\1\0\1\7\6\0\3\7"+ + "\1\0\1\7\1\0\1\7\1\0\15\7\2\0\2\7"+ + "\11\0\1\7\1\0\6\7\1\0\1\7\6\0\1\u0243"+ + "\2\7\1\0\1\7\1\0\1\7\1\0\15\7\2\0"+ + "\2\7\11\0\1\7\1\0\1\7\1\u0244\4\7\1\0"+ + "\1\7\6\0\3\7\1\0\1\7\1\0\1\7\1\0"+ + "\15\7\2\0\2\7\11\0\1\7\1\0\6\7\1\0"+ + "\1\7\6\0\3\7\1\0\1\7\1\u0245\1\u0246\1\0"+ + "\15\7\42\0\2\u0247\20\0\2\7\11\0\1\7\1\0"+ + "\6\7\1\0\1\7\6\0\3\7\1\0\1\7\1\u0247"+ + "\1\u0248\1\0\15\7\2\0\2\7\11\0\1\7\1\0"+ + "\6\7\1\0\1\7\6\0\3\7\1\0\1\7\1\0"+ + "\1\7\1\0\7\7\1\u0138\5\7\2\0\2\7\11\0"+ + "\1\u0249\1\0\6\7\1\0\1\7\6\0\3\7\1\0"+ + "\1\7\1\0\1\7\1\0\15\7\2\0\2\7\11\0"+ + "\1\7\1\0\1\7\1\u024a\4\7\1\0\1\7\6\0"+ + "\3\7\1\0\1\7\1\0\1\7\1\0\15\7\2\0"+ + "\2\7\11\0\1\7\1\0\6\7\1\u024b\1\u024c\6\0"+ + "\3\7\1\0\1\7\1\0\1\7\1\0\15\7\2\0"+ + "\2\7\11\0\1\7\1\0\1\7\1\u024d\4\7\1\0"+ + "\1\7\6\0\3\7\1\0\1\7\1\0\1\7\1\0"+ + "\15\7\17\0\1\u024e\44\0\2\7\11\0\1\7\1\0"+ + "\1\u010d\5\7\1\0\1\7\6\0\3\7\1\0\1\7"+ + "\1\0\1\7\1\0\15\7\25\0\2\u024e\35\0\2\7"+ + "\11\0\1\7\1\0\6\7\1\u024e\1\273\6\0\3\7"+ + "\1\0\1\7\1\0\1\7\1\0\15\7\2\0\2\7"+ + "\11\0\1\7\1\0\2\7\1\367\3\7\1\0\1\7"+ + "\6\0\1\7\1\u024f\1\7\1\0\1\7\1\0\1\7"+ + "\1\0\15\7\2\0\2\7\11\0\1\7\1\0\1\7"+ + "\1\u0250\4\7\1\0\1\7\6\0\3\7\1\0\1\7"+ + "\1\0\1\7\1\0\15\7\2\0\2\7\11\0\1\u0251"+ + "\1\0\6\7\1\0\1\7\6\0\3\7\1\0\1\7"+ + "\1\0\1\7\1\0\15\7\2\0\2\7\11\0\1\7"+ + "\1\0\1\7\1\u0252\4\7\1\0\1\7\6\0\3\7"+ + "\1\0\1\7\1\0\1\7\1\0\15\7\17\0\1\u0253"+ + "\44\0\2\7\11\0\1\7\1\0\1\u0254\5\7\1\0"+ + "\1\7\6\0\3\7\1\0\1\7\1\0\1\7\1\0"+ + "\15\7\2\0\2\7\11\0\1\7\1\0\6\7\1\0"+ + "\1\7\6\0\3\7\1\0\1\7\1\0\1\7\1\0"+ + "\3\7\1\u0255\11\7\2\0\2\7\11\0\1\7\1\0"+ + "\5\7\1\273\1\0\1\7\6\0\3\7\1\0\1\7"+ + "\1\0\1\7\1\0\15\7\2\0\2\7\11\0\1\7"+ + "\1\0\1\u0256\5\7\1\0\1\7\6\0\3\7\1\0"+ + "\1\7\1\0\1\7\1\0\15\7\51\0\1\u0257\34\0"+ + "\1\u0258\37\0\2\7\11\0\1\7\1\0\6\7\1\0"+ + "\1\7\6\0\3\7\1\0\1\7\1\0\1\7\1\0"+ + "\4\7\1\u0259\10\7\2\0\2\7\11\0\1\7\1\0"+ + "\5\7\1\u025a\1\0\1\7\6\0\3\7\1\0\1\7"+ + "\1\0\1\7\1\0\15\7\2\0\2\7\11\0\1\7"+ + "\1\0\6\7\1\0\1\7\6\0\1\7\1\u012c\1\7"+ + "\1\0\1\7\1\0\1\7\1\0\15\7\51\0\1\u0170"+ + "\12\0\2\7\11\0\1\7\1\0\6\7\1\0\1\7"+ + "\6\0\3\7\1\0\1\7\1\0\1\7\1\0\4\7"+ + "\1\324\10\7\15\0\1\225\46\0\2\7\11\0\1\233"+ + "\1\0\6\7\1\0\1\7\6\0\3\7\1\0\1\7"+ + "\1\0\1\7\1\0\15\7\2\0\2\7\11\0\1\7"+ + "\1\0\1\7\1\u025b\4\7\1\0\1\7\6\0\3\7"+ + "\1\0\1\7\1\0\1\7\1\0\15\7\2\0\2\7"+ + "\11\0\1\7\1\0\6\7\1\0\1\7\6\0\3\7"+ + "\1\0\1\7\1\0\1\7\1\u025c\1\u025d\14\7\2\0"+ + "\2\7\11\0\1\7\1\0\6\7\1\0\1\7\6\0"+ + "\3\7\1\0\1\7\1\0\1\7\1\0\4\7\1\u025e"+ + "\10\7\44\0\2\u025f\16\0\2\7\11\0\1\7\1\0"+ + "\6\7\1\0\1\7\6\0\3\7\1\0\1\7\1\0"+ + "\1\7\1\u025f\1\u0260\14\7\36\0\1\u0170\10\0\1\u0131"+ + "\14\0\2\7\11\0\1\7\1\0\6\7\1\0\1\7"+ + "\6\0\1\7\1\324\1\7\1\0\1\7\1\0\1\7"+ + "\1\0\2\7\1\u0138\12\7\2\0\2\7\11\0\1\u0261"+ + "\1\0\6\7\1\0\1\7\6\0\3\7\1\0\1\7"+ + "\1\0\1\7\1\0\15\7\2\0\2\7\11\0\1\7"+ + "\1\0\6\7\1\u0262\1\u0263\6\0\3\7\1\0\1\7"+ + "\1\0\1\7\1\0\15\7\22\0\1\225\41\0\2\7"+ + "\11\0\1\7\1\0\3\7\1\233\2\7\1\0\1\7"+ + "\6\0\3\7\1\0\1\7\1\0\1\7\1\0\15\7"+ + "\2\0\2\7\11\0\1\u01b1\1\0\6\7\1\0\1\7"+ + "\6\0\3\7\1\0\1\7\1\0\1\7\1\0\15\7"+ + "\2\0\2\7\11\0\1\7\1\0\6\7\1\0\1\7"+ + "\6\0\3\7\1\0\1\u0264\1\0\1\7\1\0\15\7"+ + "\2\0\2\7\11\0\1\7\1\0\6\7\1\u0265\1\u0266"+ + "\6\0\3\7\1\0\1\7\1\0\1\7\1\0\15\7"+ + "\2\0\2\7\11\0\1\7\1\0\4\7\1\u010d\1\7"+ + "\1\0\1\7\6\0\3\7\1\0\1\7\1\0\1\7"+ + "\1\0\15\7\50\0\1\u0267\13\0\2\7\11\0\1\7"+ + "\1\0\6\7\1\0\1\7\6\0\3\7\1\0\1\7"+ + "\1\0\1\7\1\0\3\7\1\u0268\11\7\44\0\2\u0269"+ + "\16\0\2\7\11\0\1\7\1\0\6\7\1\0\1\7"+ + "\6\0\3\7\1\0\1\7\1\0\1\7\1\u0269\1\u026a"+ + "\14\7\2\0\2\7\11\0\1\7\1\0\6\7\1\0"+ + "\1\7\6\0\3\7\1\0\1\u026b\1\0\1\7\1\0"+ + "\15\7\2\0\2\7\11\0\1\7\1\0\6\7\1\0"+ + "\1\7\6\0\3\7\1\0\1\7\1\0\1\7\1\u023e"+ + "\1\367\14\7\46\0\1\u026c\15\0\2\7\11\0\1\7"+ + "\1\0\6\7\1\0\1\7\6\0\3\7\1\0\1\7"+ + "\1\0\1\7\1\0\1\7\1\u026d\13\7\2\0\2\7"+ + "\11\0\1\7\1\0\6\7\1\0\1\7\6\0\1\323"+ + "\2\7\1\0\1\7\1\0\1\7\1\0\15\7\2\0"+ + "\2\7\11\0\1\7\1\0\1\233\5\7\1\0\1\7"+ + "\6\0\3\7\1\0\1\7\1\0\1\7\1\0\15\7"+ + "\20\0\1\u0237\43\0\2\7\11\0\1\7\1\0\1\7"+ + "\1\326\4\7\1\0\1\7\6\0\3\7\1\0\1\7"+ + "\1\0\1\7\1\0\15\7\2\0\2\7\11\0\1\7"+ + "\1\0\2\7\1\u026e\3\7\1\0\1\7\6\0\3\7"+ + "\1\0\1\7\1\0\1\7\1\0\15\7\2\0\2\7"+ + "\11\0\1\7\1\0\6\7\1\0\1\7\6\0\3\7"+ + "\1\0\1\7\1\0\1\7\1\0\1\7\1\u0128\13\7"+ + "\2\0\2\7\11\0\1\7\1\0\6\7\1\0\1\7"+ + "\6\0\3\7\1\0\1\7\1\0\1\7\1\122\1\123"+ + "\14\7\35\0\1\u026f\74\0\1\u0270\26\0\1\u0271\71\0"+ + "\2\u0170\35\0\2\7\11\0\1\7\1\0\6\7\1\0"+ + "\1\7\6\0\1\u0272\2\7\1\0\1\7\1\0\1\7"+ + "\1\0\15\7\2\0\2\7\11\0\1\7\1\0\6\7"+ + "\1\0\1\7\6\0\3\7\1\0\1\7\1\0\1\7"+ + "\1\0\3\7\1\u0273\11\7\2\0\2\7\11\0\1\7"+ + "\1\0\6\7\1\0\1\7\6\0\3\7\1\0\1\7"+ + "\1\u024e\1\273\1\0\15\7\2\0\2\7\11\0\1\7"+ + "\1\0\1\u0274\5\7\1\0\1\7\6\0\3\7\1\0"+ + "\1\7\1\0\1\7\1\0\15\7\2\0\2\7\11\0"+ + "\1\7\1\0\6\7\1\0\1\7\6\0\3\7\1\0"+ + "\1\7\1\0\1\7\1\u0275\1\u0276\14\7\35\0\1\u0277"+ + "\26\0\2\7\11\0\1\7\1\0\6\7\1\0\1\7"+ + "\6\0\1\u0278\2\7\1\0\1\7\1\0\1\7\1\0"+ + "\15\7\35\0\1\u0279\61\0\1\u027a\43\0\1\u027b\65\0"+ + "\1\u027c\73\0\1\u027d\43\0\1\376\77\0\1\u027e\44\0"+ + "\1\u027f\56\0\1\u0280\70\0\1\u0281\54\0\1\u0282\114\0"+ + "\1\u0170\11\0\2\7\11\0\1\7\1\0\6\7\1\0"+ + "\1\7\6\0\1\u0283\2\7\1\0\1\7\1\0\1\7"+ + "\1\0\15\7\2\0\2\7\11\0\1\7\1\0\6\7"+ + "\1\0\1\7\6\0\1\u0284\2\7\1\0\1\7\1\0"+ + "\1\7\1\0\15\7\2\0\2\7\11\0\1\7\1\0"+ + "\1\u0285\5\7\1\0\1\7\6\0\3\7\1\0\1\7"+ + "\1\0\1\7\1\0\15\7\2\0\2\7\11\0\1\7"+ + "\1\0\4\7\1\u0286\1\7\1\0\1\7\6\0\3\7"+ + "\1\0\1\7\1\0\1\7\1\0\15\7\2\0\2\7"+ + "\11\0\1\7\1\0\6\7\1\0\1\7\6\0\1\u0287"+ + "\2\7\1\0\1\7\1\0\1\7\1\0\15\7\2\0"+ + "\2\7\11\0\1\7\1\0\1\316\5\7\1\0\1\7"+ + "\6\0\3\7\1\0\1\7\1\0\1\7\1\0\15\7"+ + "\2\0\2\7\11\0\1\7\1\0\6\7\1\0\1\7"+ + "\6\0\1\u0288\2\7\1\0\1\7\1\0\1\7\1\0"+ + "\15\7\2\0\2\7\11\0\1\7\1\0\1\7\1\u0289"+ + "\4\7\1\0\1\7\6\0\3\7\1\0\1\7\1\0"+ + "\1\7\1\0\15\7\2\0\2\7\11\0\1\u0121\1\0"+ + "\6\7\1\0\1\7\6\0\3\7\1\0\1\7\1\0"+ + "\1\7\1\0\15\7\2\0\2\7\11\0\1\7\1\0"+ + "\5\7\1\u028a\1\0\1\7\6\0\3\7\1\0\1\7"+ + "\1\0\1\7\1\0\15\7\2\0\2\7\11\0\1\7"+ + "\1\0\1\u028b\5\7\1\0\1\7\6\0\3\7\1\0"+ + "\1\7\1\0\1\7\1\0\15\7\35\0\1\u028c\41\0"+ + "\1\u028d\46\0\2\7\11\0\1\7\1\0\6\7\1\0"+ + "\1\7\6\0\1\7\1\347\1\7\1\0\1\7\1\0"+ + "\1\7\1\0\15\7\15\0\1\u024e\63\0\1\u028e\44\0"+ + "\2\7\11\0\1\7\1\0\1\u028f\5\7\1\0\1\7"+ + "\6\0\3\7\1\0\1\7\1\0\1\7\1\0\15\7"+ + "\2\0\2\7\11\0\1\7\1\0\4\7\1\u0290\1\7"+ + "\1\0\1\7\6\0\3\7\1\0\1\7\1\0\1\7"+ + "\1\0\15\7\20\0\1\u0291\43\0\2\7\11\0\1\7"+ + "\1\0\1\7\1\u0292\4\7\1\0\1\7\6\0\3\7"+ + "\1\0\1\7\1\0\1\7\1\0\15\7\2\0\2\7"+ + "\11\0\1\7\1\0\6\7\1\0\1\7\6\0\3\7"+ + "\1\0\1\7\1\0\1\7\1\u0293\1\u0294\14\7\50\0"+ + "\1\u0170\34\0\1\u0295\102\0\2\u0296\31\0\1\u0297\46\0"+ + "\2\7\11\0\1\7\1\0\4\7\1\u0298\1\7\1\0"+ + "\1\7\6\0\3\7\1\0\1\7\1\0\1\7\1\0"+ + "\15\7\2\0\2\7\11\0\1\7\1\0\6\7\1\0"+ + "\1\7\6\0\3\7\1\0\1\7\1\0\1\7\1\u0296"+ + "\1\u0299\14\7\2\0\2\7\11\0\1\u029a\1\0\6\7"+ + "\1\0\1\7\6\0\3\7\1\0\1\7\1\0\1\7"+ + "\1\0\15\7\55\0\1\u029b\6\0\2\7\11\0\1\7"+ + "\1\0\6\7\1\0\1\7\6\0\3\7\1\0\1\7"+ + "\1\0\1\7\1\0\10\7\1\u029c\4\7\23\0\1\u029d"+ + "\40\0\2\7\11\0\1\7\1\0\4\7\1\111\1\7"+ + "\1\0\1\7\6\0\3\7\1\0\1\7\1\0\1\7"+ + "\1\0\15\7\2\0\2\7\11\0\1\7\1\0\6\7"+ + "\1\0\1\7\6\0\3\7\1\0\1\7\1\0\1\7"+ + "\1\0\1\7\1\u029e\13\7\2\0\2\7\11\0\1\7"+ + "\1\0\6\7\1\0\1\7\6\0\1\7\1\u029f\1\7"+ + "\1\0\1\7\1\0\1\7\1\0\15\7\46\0\1\u02a0"+ + "\15\0\2\7\11\0\1\7\1\0\6\7\1\0\1\7"+ + "\6\0\3\7\1\0\1\7\1\0\1\7\1\0\1\7"+ + "\1\u02a1\13\7\2\0\2\7\11\0\1\u02a2\1\0\6\7"+ + "\1\0\1\7\6\0\3\7\1\0\1\7\1\0\1\7"+ + "\1\0\15\7\2\0\2\7\11\0\1\7\1\0\6\7"+ + "\1\0\1\7\6\0\2\7\1\u02a3\1\0\1\7\1\0"+ + "\1\7\1\0\15\7\2\0\2\7\11\0\1\7\1\0"+ + "\6\7\1\0\1\7\6\0\1\7\1\u02a4\1\7\1\0"+ + "\1\7\1\0\1\7\1\0\15\7\2\0\2\7\11\0"+ + "\1\7\1\0\6\7\1\0\1\7\6\0\3\7\1\0"+ + "\1\7\1\0\1\7\1\0\4\7\1\u02a5\10\7\2\0"+ + "\2\7\11\0\1\7\1\0\4\7\1\105\1\7\1\0"+ + "\1\7\6\0\3\7\1\0\1\7\1\0\1\7\1\0"+ + "\15\7\44\0\2\227\16\0\2\7\11\0\1\7\1\0"+ + "\6\7\1\0\1\7\6\0\3\7\1\0\1\7\1\0"+ + "\1\7\1\227\1\235\14\7\2\0\2\7\11\0\1\7"+ + "\1\0\6\7\1\u0291\1\u0292\6\0\3\7\1\0\1\7"+ + "\1\0\1\7\1\0\15\7\2\0\2\7\11\0\1\7"+ + "\1\0\6\7\1\0\1\7\6\0\3\7\1\0\1\7"+ + "\1\0\1\7\1\0\4\7\1\u028a\10\7\44\0\2\u02a6"+ + "\60\0\2\u02a7\16\0\2\7\11\0\1\7\1\0\6\7"+ + "\1\0\1\7\6\0\3\7\1\0\1\7\1\0\1\7"+ + "\1\u02a6\1\u02a8\14\7\2\0\2\7\11\0\1\7\1\0"+ + "\6\7\1\0\1\7\6\0\3\7\1\0\1\7\1\0"+ + "\1\7\1\u02a7\1\u02a9\14\7\2\0\2\7\11\0\1\7"+ + "\1\0\6\7\1\0\1\7\6\0\3\7\1\0\1\7"+ + "\1\0\1\7\1\u02aa\1\u02ab\14\7\35\0\1\u0188\26\0"+ + "\2\7\11\0\1\7\1\0\6\7\1\0\1\7\6\0"+ + "\1\u018c\2\7\1\0\1\7\1\0\1\7\1\0\15\7"+ + "\2\0\2\7\11\0\1\7\1\0\4\7\1\u02ac\1\7"+ + "\1\0\1\7\6\0\3\7\1\0\1\7\1\0\1\7"+ + "\1\0\15\7\46\0\1\u0170\15\0\2\7\11\0\1\7"+ + "\1\0\6\7\1\0\1\7\6\0\3\7\1\0\1\7"+ + "\1\0\1\7\1\0\1\7\1\324\13\7\2\0\2\7"+ + "\11\0\1\7\1\0\6\7\1\0\1\7\6\0\1\7"+ + "\1\u02ad\1\7\1\0\1\7\1\0\1\7\1\0\15\7"+ + "\44\0\2\u02ae\16\0\2\7\11\0\1\7\1\0\6\7"+ + "\1\0\1\7\6\0\3\7\1\0\1\7\1\0\1\7"+ + "\1\u02ae\1\u02af\14\7\2\0\2\7\11\0\1\7\1\0"+ + "\1\7\1\u02b0\4\7\1\0\1\7\6\0\3\7\1\0"+ + "\1\7\1\0\1\7\1\0\15\7\44\0\2\u02b1\16\0"+ + "\2\7\11\0\1\7\1\0\6\7\1\0\1\7\6\0"+ + "\3\7\1\0\1\7\1\0\1\7\1\u02b1\1\u02b2\14\7"+ + "\23\0\1\u02b3\40\0\2\7\11\0\1\7\1\0\4\7"+ + "\1\u02b4\1\7\1\0\1\7\6\0\3\7\1\0\1\7"+ + "\1\0\1\7\1\0\15\7\35\0\1\u02b5\26\0\2\7"+ + "\11\0\1\7\1\0\6\7\1\0\1\7\6\0\1\u02b6"+ + "\2\7\1\0\1\7\1\0\1\7\1\0\15\7\2\0"+ + "\2\7\11\0\1\u0169\1\0\6\7\1\0\1\7\6\0"+ + "\3\7\1\0\1\7\1\0\1\7\1\0\15\7\17\0"+ + "\1\u02b7\44\0\2\7\11\0\1\7\1\0\1\u02b8\5\7"+ + "\1\0\1\7\6\0\3\7\1\0\1\7\1\0\1\7"+ + "\1\0\15\7\2\0\2\7\11\0\1\7\1\0\4\7"+ + "\1\u01e5\1\7\1\0\1\7\6\0\3\7\1\0\1\7"+ + "\1\0\1\7\1\0\15\7\17\0\1\u02b9\113\0\1\u02ba"+ + "\30\0\1\u02bb\43\0\2\7\11\0\1\7\1\0\1\u02bc"+ + "\5\7\1\0\1\7\6\0\3\7\1\0\1\7\1\0"+ + "\1\7\1\0\15\7\2\0\2\7\11\0\1\7\1\0"+ + "\6\7\1\0\1\7\6\0\3\7\1\0\1\7\1\0"+ + "\1\7\1\0\4\7\1\145\10\7\2\0\2\7\11\0"+ + "\1\7\1\0\6\7\1\0\1\7\6\0\3\7\1\0"+ + "\1\7\1\0\1\7\1\u02bd\1\u02be\14\7\37\0\1\u02bf"+ + "\24\0\2\7\11\0\1\7\1\0\6\7\1\0\1\7"+ + "\6\0\2\7\1\325\1\0\1\7\1\0\1\7\1\0"+ + "\15\7\15\0\1\u02c0\3\0\1\u02c1\1\0\1\u02c2\1\0"+ + "\2\u02c3\7\0\1\u02c4\2\0\1\u02c5\7\0\1\u02c6\12\0"+ + "\2\7\11\0\1\u02c7\1\0\2\7\1\u02c8\1\7\1\u02c9"+ + "\1\7\1\u02c3\1\u02ca\6\0\1\7\1\u02cb\1\7\1\0"+ + "\1\u02cc\1\0\1\7\1\0\4\7\1\u02cd\10\7\47\0"+ + "\1\u0211\70\0\1\u024e\26\0\1\u02ce\55\0\1\u02cf\110\0"+ + "\1\u02d0\35\0\1\u02d1\100\0\1\u02d2\45\0\2\u01bf\74\0"+ + "\1\u02c5\57\0\1\u02d3\24\0\2\7\11\0\1\7\1\0"+ + "\6\7\1\0\1\7\6\0\3\7\1\0\1\7\1\0"+ + "\1\7\1\0\2\7\1\u0192\12\7\2\0\2\7\11\0"+ + "\1\7\1\0\6\7\1\0\1\7\6\0\3\7\1\0"+ + "\1\7\1\0\1\7\1\0\11\7\1\273\3\7\2\0"+ + "\2\7\11\0\1\7\1\0\4\7\1\u02d4\1\7\1\0"+ + "\1\7\6\0\3\7\1\0\1\7\1\0\1\7\1\0"+ + "\15\7\2\0\2\7\11\0\1\7\1\0\1\u02d5\5\7"+ + "\1\0\1\7\6\0\3\7\1\0\1\7\1\0\1\7"+ + "\1\0\15\7\2\0\2\7\11\0\1\7\1\0\6\7"+ + "\1\0\1\7\6\0\3\7\1\0\1\7\1\0\1\7"+ + "\1\0\1\7\1\u02d6\13\7\2\0\2\7\11\0\1\7"+ + "\1\0\3\7\1\u02d7\2\7\1\0\1\7\6\0\3\7"+ + "\1\0\1\7\1\0\1\7\1\0\15\7\2\0\2\7"+ + "\11\0\1\7\1\0\6\7\1\0\1\7\6\0\3\7"+ + "\1\0\1\u0182\1\0\1\7\1\0\15\7\2\0\2\7"+ + "\11\0\1\7\1\0\6\7\1\0\1\7\6\0\3\7"+ + "\1\0\1\u02cc\1\0\1\7\1\0\15\7\2\0\2\7"+ + "\11\0\1\7\1\0\6\7\1\0\1\7\6\0\2\7"+ + "\1\u02d8\1\0\1\7\1\0\1\7\1\0\15\7\47\0"+ + "\1\u01b6\32\0\1\u01b6\116\0\1\u0170\6\0\2\7\11\0"+ + "\1\7\1\0\6\7\1\0\1\7\6\0\3\7\1\0"+ + "\1\7\1\0\1\7\1\0\10\7\1\324\4\7\2\0"+ + "\2\7\11\0\1\7\1\0\6\7\1\0\1\7\6\0"+ + "\3\7\1\0\1\7\1\0\1\7\1\0\10\7\1\u02d9"+ + "\4\7\37\0\1\156\24\0\2\7\11\0\1\7\1\0"+ + "\6\7\1\0\1\7\6\0\2\7\1\170\1\0\1\7"+ + "\1\0\1\7\1\0\15\7\35\0\1\u02da\26\0\2\7"+ + "\11\0\1\7\1\0\6\7\1\0\1\7\6\0\1\u02db"+ + "\2\7\1\0\1\7\1\0\1\7\1\0\15\7\46\0"+ + "\1\u02dc\32\0\1\u02dd\57\0\1\u02de\4\0\1\u02df\12\0"+ + "\1\u02e0\26\0\2\7\11\0\1\7\1\0\6\7\1\0"+ + "\1\7\6\0\3\7\1\0\1\7\1\0\1\7\1\0"+ + "\1\7\1\u02e1\13\7\2\0\2\7\11\0\1\7\1\0"+ + "\1\u02e2\5\7\1\0\1\7\6\0\3\7\1\0\1\7"+ + "\1\0\1\7\1\0\15\7\2\0\2\7\11\0\1\u02e3"+ + "\1\0\3\7\1\u02e4\2\7\1\0\1\7\6\0\1\u02e5"+ + "\2\7\1\0\1\7\1\0\1\7\1\0\15\7\25\0"+ + "\2\u02e6\35\0\2\7\11\0\1\7\1\0\6\7\1\u02e6"+ + "\1\u02e7\6\0\3\7\1\0\1\7\1\0\1\7\1\0"+ + "\15\7\41\0\1\376\22\0\2\7\11\0\1\7\1\0"+ + "\1\u02e8\5\7\1\0\1\7\6\0\3\7\1\0\1\7"+ + "\1\0\1\7\1\0\15\7\2\0\2\7\11\0\1\7"+ + "\1\0\6\7\1\0\1\7\6\0\3\7\1\0\1\7"+ + "\1\0\1\7\1\u02e9\1\u02ea\14\7\23\0\1\376\40\0"+ + "\2\7\11\0\1\7\1\0\4\7\1\316\1\7\1\0"+ + "\1\7\6\0\3\7\1\0\1\7\1\0\1\7\1\0"+ + "\15\7\2\0\2\7\11\0\1\7\1\0\6\7\1\u02eb"+ + "\1\u02ec\6\0\3\7\1\0\1\7\1\0\1\7\1\0"+ + "\15\7\2\0\2\7\11\0\1\7\1\0\6\7\1\0"+ + "\1\7\6\0\3\7\1\0\1\7\1\0\1\7\1\0"+ + "\5\7\1\u0138\7\7\2\0\2\7\11\0\1\7\1\0"+ + "\6\7\1\0\1\7\6\0\2\7\1\135\1\0\1\7"+ + "\1\0\1\7\1\0\15\7\2\0\2\7\11\0\1\7"+ + "\1\0\6\7\1\0\1\7\6\0\2\7\1\u02ed\1\0"+ + "\1\7\1\0\1\7\1\0\15\7\47\0\1\u02ee\61\0"+ + "\1\u02ef\14\0\2\7\11\0\1\7\1\0\6\7\1\0"+ + "\1\7\6\0\3\7\1\0\1\7\1\0\1\7\1\0"+ + "\2\7\1\u02f0\12\7\2\0\2\7\11\0\1\7\1\0"+ + "\6\7\1\0\1\7\6\0\3\7\1\0\1\7\1\0"+ + "\1\7\1\0\2\7\1\u02f1\12\7\57\0\1\u02f2\4\0"+ + "\2\7\11\0\1\7\1\0\6\7\1\0\1\7\6\0"+ + "\3\7\1\0\1\7\1\0\1\7\1\0\12\7\1\u02f3"+ + "\2\7\2\0\2\7\11\0\1\7\1\0\1\7\1\u0192"+ + "\4\7\1\0\1\7\6\0\3\7\1\0\1\7\1\0"+ + "\1\7\1\0\15\7\2\0\2\7\11\0\1\7\1\0"+ + "\6\7\1\0\1\7\6\0\3\7\1\0\1\7\1\0"+ + "\1\7\1\0\7\7\1\u02f4\5\7\37\0\1\u02f5\24\0"+ + "\2\7\11\0\1\7\1\0\6\7\1\0\1\7\6\0"+ + "\2\7\1\u02f6\1\0\1\7\1\0\1\7\1\0\15\7"+ + "\2\0\2\7\11\0\1\7\1\0\4\7\1\u02f7\1\7"+ + "\1\0\1\7\6\0\3\7\1\0\1\7\1\0\1\7"+ + "\1\0\15\7\56\0\1\u02f8\5\0\2\7\11\0\1\7"+ + "\1\0\6\7\1\0\1\7\6\0\3\7\1\0\1\7"+ + "\1\0\1\7\1\0\11\7\1\u02f9\3\7\20\0\1\u0130"+ + "\43\0\2\7\11\0\1\7\1\0\1\7\1\u0137\4\7"+ + "\1\0\1\7\6\0\3\7\1\0\1\7\1\0\1\7"+ + "\1\0\15\7\47\0\1\u024e\14\0\2\7\11\0\1\7"+ + "\1\0\6\7\1\0\1\7\6\0\3\7\1\0\1\7"+ + "\1\0\1\7\1\0\2\7\1\273\12\7\46\0\1\u02fa"+ + "\15\0\2\7\11\0\1\7\1\0\6\7\1\0\1\7"+ + "\6\0\3\7\1\0\1\7\1\0\1\7\1\0\1\7"+ + "\1\u02fb\13\7\57\0\1\u02fc\25\0\1\u0170\62\0\1\u02fd"+ + "\37\0\2\7\11\0\1\7\1\0\6\7\1\0\1\7"+ + "\6\0\3\7\1\0\1\7\1\0\1\7\1\0\12\7"+ + "\1\u02fe\2\7\54\0\1\u0188\7\0\2\7\11\0\1\7"+ + "\1\0\6\7\1\0\1\7\6\0\3\7\1\0\1\7"+ + "\1\0\1\7\1\0\7\7\1\u018c\5\7\35\0\1\225"+ + "\61\0\1\u02ff\15\0\1\u0300\56\0\1\u0301\35\0\1\310"+ + "\101\0\2\u0302\1\u0303\33\0\1\u0304\20\0\1\356\43\0"+ + "\1\u0305\75\0\1\u0306\24\0\2\7\11\0\1\7\1\0"+ + "\6\7\1\0\1\7\6\0\1\u013b\2\7\1\0\1\7"+ + "\1\0\1\7\1\0\6\7\1\105\6\7\2\0\2\7"+ + "\11\0\1\7\1\0\6\7\1\0\1\7\6\0\3\7"+ + "\1\0\1\7\1\0\1\7\1\0\3\7\1\u0307\11\7"+ + "\2\0\2\7\11\0\1\7\1\0\5\7\1\311\1\0"+ + "\1\7\6\0\3\7\1\0\1\7\1\0\1\7\1\0"+ + "\15\7\2\0\2\7\11\0\1\7\1\0\6\7\1\0"+ + "\1\7\6\0\3\7\1\0\1\7\1\0\1\7\1\u0302"+ + "\1\u0308\1\u0309\13\7\2\0\2\7\11\0\1\7\1\0"+ + "\1\7\1\u030a\4\7\1\0\1\7\6\0\3\7\1\0"+ + "\1\351\1\0\1\7\1\0\15\7\2\0\2\7\11\0"+ + "\1\7\1\0\4\7\1\u018e\1\7\1\0\1\7\6\0"+ + "\3\7\1\0\1\7\1\0\1\7\1\0\15\7\2\0"+ + "\2\7\11\0\1\7\1\0\6\7\1\0\1\7\6\0"+ + "\2\7\1\u030b\1\0\1\7\1\0\1\7\1\0\15\7"+ + "\17\0\1\u030c\101\0\1\u0305\37\0\1\u030d\103\0\1\u0170"+ + "\45\0\1\u030e\104\0\1\u030f\15\0\2\7\11\0\1\7"+ + "\1\0\1\u0310\5\7\1\0\1\7\6\0\3\7\1\0"+ + "\1\7\1\0\1\7\1\0\15\7\2\0\2\7\11\0"+ + "\1\7\1\0\6\7\1\0\1\7\6\0\2\7\1\u018e"+ + "\1\0\1\7\1\0\1\7\1\0\15\7\2\0\2\7"+ + "\11\0\1\u0311\1\0\6\7\1\0\1\7\6\0\3\7"+ + "\1\0\1\7\1\0\1\7\1\0\15\7\2\0\2\7"+ + "\11\0\1\7\1\0\6\7\1\0\1\7\6\0\2\7"+ + "\1\324\1\0\1\7\1\0\1\7\1\0\15\7\2\0"+ + "\2\7\11\0\1\7\1\0\6\7\1\0\1\7\6\0"+ + "\3\7\1\0\1\7\1\0\1\7\1\0\1\7\1\u0312"+ + "\13\7\2\0\2\7\11\0\1\7\1\0\1\u0313\5\7"+ + "\1\0\1\7\6\0\3\7\1\0\1\7\1\0\1\7"+ + "\1\0\15\7\47\0\1\u0314\14\0\2\7\11\0\1\7"+ + "\1\0\6\7\1\0\1\7\6\0\3\7\1\0\1\7"+ + "\1\0\1\7\1\0\2\7\1\u0315\12\7\17\0\1\u0247"+ + "\117\0\1\u0316\31\0\2\u025f\27\0\1\u0317\42\0\1\u0318"+ + "\45\0\1\u0319\40\0\2\7\11\0\1\7\1\0\1\u0248"+ + "\5\7\1\0\1\7\6\0\3\7\1\0\1\7\1\0"+ + "\1\7\1\0\15\7\2\0\2\7\11\0\1\7\1\0"+ + "\6\7\1\0\1\7\6\0\3\7\1\0\1\7\1\0"+ + "\1\7\1\0\10\7\1\u031a\4\7\2\0\2\7\11\0"+ + "\1\7\1\0\6\7\1\u025f\1\u0260\6\0\3\7\1\0"+ + "\1\7\1\0\1\7\1\0\11\7\1\u031b\3\7\2\0"+ + "\2\7\11\0\1\7\1\0\6\7\1\0\1\7\6\0"+ + "\2\7\1\u031c\1\0\1\7\1\0\1\7\1\0\15\7"+ + "\2\0\2\7\11\0\1\7\1\0\4\7\1\u031d\1\7"+ + "\1\0\1\7\6\0\3\7\1\0\1\7\1\0\1\7"+ + "\1\0\15\7\46\0\1\u0303\15\0\2\7\11\0\1\7"+ + "\1\0\6\7\1\0\1\7\6\0\3\7\1\0\1\7"+ + "\1\0\1\7\1\0\1\7\1\u0309\13\7\2\0\2\7"+ + "\11\0\1\7\1\0\6\7\1\0\1\7\6\0\3\7"+ + "\1\0\1\7\1\0\1\7\1\0\2\7\1\u031e\12\7"+ + "\25\0\2\u031f\35\0\2\7\11\0\1\7\1\0\6\7"+ + "\1\u031f\1\u0320\6\0\3\7\1\0\1\7\1\0\1\7"+ + "\1\0\15\7\25\0\2\u0281\35\0\2\7\11\0\1\7"+ + "\1\0\6\7\1\u0281\1\u028a\6\0\3\7\1\0\1\7"+ + "\1\0\1\7\1\0\15\7\2\0\2\7\11\0\1\7"+ + "\1\0\5\7\1\u0236\1\0\1\7\6\0\3\7\1\0"+ + "\1\7\1\0\1\7\1\0\15\7\51\0\1\u0321\61\0"+ + "\1\u0322\12\0\2\7\11\0\1\7\1\0\6\7\1\0"+ + "\1\7\6\0\3\7\1\0\1\7\1\0\1\7\1\0"+ + "\4\7\1\u0323\10\7\2\0\2\7\11\0\1\7\1\0"+ + "\6\7\1\0\1\7\6\0\3\7\1\0\1\7\1\0"+ + "\1\7\1\0\4\7\1\u0324\10\7\15\0\1\u0325\46\0"+ + "\2\7\11\0\1\u0326\1\0\6\7\1\0\1\7\6\0"+ + "\3\7\1\0\1\7\1\0\1\7\1\0\15\7\2\0"+ + "\2\7\11\0\1\7\1\0\6\7\1\0\1\7\6\0"+ + "\2\7\1\u0327\1\0\1\7\1\0\1\7\1\0\15\7"+ + "\35\0\1\u024e\26\0\2\7\11\0\1\7\1\0\6\7"+ + "\1\0\1\7\6\0\1\273\2\7\1\0\1\7\1\0"+ + "\1\7\1\0\15\7\2\0\2\7\11\0\1\7\1\0"+ + "\6\7\1\0\1\7\6\0\1\u0328\2\7\1\0\1\7"+ + "\1\0\1\7\1\0\15\7\15\0\1\u0329\46\0\2\7"+ + "\11\0\1\u032a\1\0\6\7\1\0\1\7\6\0\3\7"+ + "\1\0\1\7\1\0\1\7\1\0\15\7\24\0\1\u032b"+ + "\12\0\1\u032c\24\0\2\7\11\0\1\7\1\0\5\7"+ + "\1\u032d\1\0\1\7\6\0\2\7\1\u032e\1\0\1\7"+ + "\1\0\1\7\1\0\15\7\37\0\1\u032f\45\0\1\u0330"+ + "\40\0\2\7\11\0\1\7\1\0\6\7\1\0\1\7"+ + "\6\0\2\7\1\u01c8\1\0\1\7\1\0\1\7\1\0"+ + "\15\7\17\0\1\u0331\106\0\2\310\52\0\1\u0332\77\0"+ + "\1\u0333\25\0\1\u0334\64\0\1\u0335\56\0\1\u0170\65\0"+ + "\1\u01a2\37\0\2\7\11\0\1\7\1\0\6\7\1\0"+ + "\1\7\6\0\1\7\1\u01fe\1\7\1\0\1\7\1\0"+ + "\1\7\1\0\15\7\2\0\2\7\11\0\1\7\1\0"+ + "\6\7\1\0\1\7\6\0\3\7\1\0\1\7\1\0"+ + "\1\7\1\0\7\7\1\u0336\5\7\2\0\2\7\11\0"+ + "\1\7\1\0\1\7\1\u02a4\4\7\1\0\1\7\6\0"+ + "\3\7\1\0\1\7\1\0\1\7\1\0\15\7\2\0"+ + "\2\7\11\0\1\7\1\0\4\7\1\u0337\1\7\1\0"+ + "\1\7\6\0\3\7\1\0\1\7\1\0\1\7\1\0"+ + "\15\7\2\0\2\7\11\0\1\7\1\0\5\7\1\u01b1"+ + "\1\0\1\7\6\0\3\7\1\0\1\7\1\0\1\7"+ + "\1\0\15\7\23\0\1\u0338\75\0\1\u0339\70\0\1\u033a"+ + "\37\0\1\u033b\37\0\2\7\11\0\1\7\1\0\4\7"+ + "\1\u033c\1\7\1\0\1\7\6\0\3\7\1\0\1\7"+ + "\1\0\1\7\1\0\15\7\2\0\2\7\11\0\1\7"+ + "\1\0\6\7\1\0\1\7\6\0\2\7\1\u02f7\1\0"+ + "\1\7\1\0\1\7\1\0\15\7\2\0\2\7\11\0"+ + "\1\7\1\0\5\7\1\u0204\1\0\1\7\6\0\3\7"+ + "\1\0\1\7\1\0\1\7\1\0\15\7\2\0\2\7"+ + "\11\0\1\7\1\0\6\7\1\0\1\7\6\0\3\7"+ + "\1\0\1\7\1\0\1\7\1\113\1\114\14\7\50\0"+ + "\1\u033d\13\0\2\7\11\0\1\7\1\0\6\7\1\0"+ + "\1\7\6\0\3\7\1\0\1\7\1\0\1\7\1\0"+ + "\3\7\1\u033e\11\7\61\0\1\u033f\15\0\1\u0340\112\0"+ + "\1\u0341\71\0\1\u0170\5\0\2\7\11\0\1\7\1\0"+ + "\6\7\1\0\1\7\6\0\3\7\1\0\1\7\1\0"+ + "\1\7\1\0\14\7\1\u0342\2\0\2\7\11\0\1\u0343"+ + "\1\0\6\7\1\0\1\7\6\0\3\7\1\0\1\7"+ + "\1\0\1\7\1\0\15\7\2\0\2\7\11\0\1\7"+ + "\1\0\6\7\1\0\1\7\6\0\3\7\1\0\1\7"+ + "\1\0\1\7\1\0\1\7\1\355\13\7\2\0\2\7"+ + "\11\0\1\7\1\0\6\7\1\0\1\7\6\0\3\7"+ + "\1\0\1\7\1\0\1\7\1\0\11\7\1\324\3\7"+ + "\2\0\2\7\11\0\1\7\1\0\4\7\1\u0344\1\7"+ + "\1\0\1\7\6\0\3\7\1\0\1\7\1\0\1\7"+ + "\1\0\15\7\50\0\1\u0345\13\0\2\7\11\0\1\7"+ + "\1\0\6\7\1\0\1\7\6\0\3\7\1\0\1\7"+ + "\1\0\1\7\1\0\3\7\1\u0346\11\7\24\0\1\u0347"+ + "\54\0\1\u0348\44\0\2\7\11\0\1\7\1\0\5\7"+ + "\1\u020d\1\0\1\7\6\0\3\7\1\0\1\7\1\0"+ + "\1\7\1\0\15\7\2\0\2\7\11\0\1\7\1\0"+ + "\1\u0349\5\7\1\0\1\7\6\0\3\7\1\0\1\7"+ + "\1\0\1\7\1\0\15\7\36\0\1\u034a\25\0\2\7"+ + "\11\0\1\7\1\0\6\7\1\0\1\7\6\0\1\7"+ + "\1\u034b\1\7\1\0\1\7\1\0\1\7\1\0\15\7"+ + "\2\0\2\7\11\0\1\7\1\0\6\7\1\0\1\7"+ + "\6\0\3\7\1\0\1\7\1\0\1\7\1\0\11\7"+ + "\1\u0164\3\7\22\0\1\u02df\41\0\2\7\11\0\1\7"+ + "\1\0\3\7\1\u02e4\2\7\1\0\1\7\6\0\3\7"+ + "\1\0\1\7\1\0\1\7\1\0\15\7\44\0\2\u034c"+ + "\51\0\1\u034d\26\0\2\7\11\0\1\7\1\0\6\7"+ + "\1\0\1\7\6\0\3\7\1\0\1\7\1\0\1\7"+ + "\1\u034c\1\u034e\14\7\2\0\2\7\11\0\1\7\1\0"+ + "\6\7\1\0\1\7\6\0\1\u034f\2\7\1\0\1\7"+ + "\1\0\1\7\1\0\15\7\37\0\1\u012d\77\0\1\u0350"+ + "\21\0\1\u0305\67\0\1\375\106\0\1\u0351\50\0\1\134"+ + "\71\0\1\u0170\14\0\2\7\11\0\1\7\1\0\6\7"+ + "\1\0\1\7\6\0\3\7\1\0\1\7\1\0\1\7"+ + "\1\0\3\7\1\u0352\11\7\2\0\2\7\11\0\1\7"+ + "\1\0\6\7\1\0\1\7\6\0\3\7\1\0\1\7"+ + "\1\0\1\7\1\0\2\7\1\324\12\7\41\0\1\u01bc"+ + "\55\0\1\u0353\66\0\2\u01c6\62\0\2\u023e\16\0\2\7"+ + "\11\0\1\7\1\0\6\7\1\0\1\7\6\0\3\7"+ + "\1\0\1\u01bd\1\0\1\7\1\0\15\7\37\0\1\u0354"+ + "\24\0\2\7\11\0\1\7\1\0\6\7\1\0\1\7"+ + "\6\0\2\7\1\u0355\1\0\1\7\1\0\1\7\1\0"+ + "\15\7\2\0\1\u0356\12\0\1\u0357\1\0\1\u0358\1\u0359"+ + "\1\u035a\1\u035b\1\u035c\1\u035d\2\u035e\7\0\1\u035f\1\u0360"+ + "\4\0\2\u0361\1\u0362\1\u0363\1\u0364\1\u0365\1\u0366\44\0"+ + "\1\u0367\45\0\1\u0211\42\0\1\u0368\1\7\11\0\1\u0369"+ + "\1\0\1\u036a\1\u036b\1\u036c\1\u036d\1\u036e\1\u036f\1\u035e"+ + "\1\u0370\6\0\1\7\1\u0371\1\u0372\1\0\1\7\1\0"+ + "\1\7\1\u0361\1\u0373\1\u0374\1\u0375\1\u0376\1\u0377\1\u0378"+ + "\7\7\2\0\2\7\11\0\1\7\1\0\6\7\1\0"+ + "\1\7\6\0\1\u0379\2\7\1\0\1\7\1\0\1\7"+ + "\1\0\15\7\2\0\2\7\11\0\1\7\1\0\1\7"+ + "\1\u037a\4\7\1\0\1\7\6\0\3\7\1\0\1\7"+ + "\1\0\1\7\1\0\15\7\24\0\1\u037b\37\0\2\7"+ + "\11\0\1\7\1\0\5\7\1\u037c\1\0\1\7\6\0"+ + "\3\7\1\0\1\7\1\0\1\7\1\0\15\7\44\0"+ + "\2\122\63\0\1\u037d\14\0\2\7\11\0\1\7\1\0"+ + "\6\7\1\0\1\7\6\0\3\7\1\0\1\7\1\0"+ + "\1\7\1\0\2\7\1\u037e\12\7\25\0\2\u037f\35\0"+ + "\2\7\11\0\1\7\1\0\6\7\1\u037f\1\u0380\6\0"+ + "\3\7\1\0\1\7\1\0\1\7\1\0\15\7\50\0"+ + "\1\u0381\36\0\2\u0382\35\0\2\7\11\0\1\7\1\0"+ + "\6\7\1\0\1\7\6\0\3\7\1\0\1\7\1\0"+ + "\1\7\1\0\3\7\1\u0383\11\7\2\0\2\7\11\0"+ + "\1\7\1\0\6\7\1\u0382\1\u0384\6\0\3\7\1\0"+ + "\1\7\1\0\1\7\1\0\15\7\17\0\1\u0385\66\0"+ + "\1\u0386\37\0\2\7\11\0\1\7\1\0\5\7\1\u0387"+ + "\1\0\1\7\6\0\3\7\1\0\1\7\1\0\1\7"+ + "\1\0\15\7\46\0\1\u0104\57\0\2\u0340\16\0\2\7"+ + "\11\0\1\7\1\0\6\7\1\0\1\7\6\0\3\7"+ + "\1\0\1\7\1\0\1\7\1\u0340\1\u0343\14\7\37\0"+ + "\1\u0388\57\0\1\u0389\41\0\1\u038a\2\0\1\u038b\2\0"+ + "\1\u038c\53\0\1\u038d\5\0\1\u038e\106\0\1\u038f\26\0"+ + "\1\u0390\21\0\1\u0391\4\0\2\u0392\52\0\1\u0393\7\0"+ + "\1\u0394\30\0\1\u0395\3\0\1\u0396\1\0\1\u0397\13\0"+ + "\1\u0398\37\0\1\u0399\1\0\1\u039a\24\0\2\u039b\1\u039c"+ + "\1\0\1\u039d\1\u039e\1\u039f\24\0\1\u03a0\21\0\1\u03a1"+ + "\4\0\2\u03a2\51\0\1\u03a3\12\0\1\u03a4\36\0\2\u03a5"+ + "\6\0\1\u03a6\47\0\1\u03a7\1\u03a8\24\0\1\u03a9\25\0"+ + "\1\u03aa\21\0\1\u03ab\42\0\1\u03ac\1\u03ad\2\0\1\u03ae"+ + "\12\0\1\u03af\61\0\1\u03b0\4\0\2\u03b1\34\0\1\u03b2"+ + "\60\0\1\u0211\44\0\2\7\11\0\1\7\1\0\6\7"+ + "\1\0\1\7\6\0\2\7\1\u03b3\1\0\1\7\1\0"+ + "\1\7\1\0\15\7\2\0\2\7\11\0\1\7\1\0"+ + "\6\7\1\0\1\7\6\0\1\u03b4\2\7\1\0\1\7"+ + "\1\0\1\7\1\0\15\7\2\0\2\7\11\0\1\u03b5"+ + "\1\0\1\7\1\u03b6\2\7\1\u03b7\1\7\1\0\1\7"+ + "\6\0\3\7\1\0\1\7\1\0\1\7\1\0\15\7"+ + "\2\0\2\7\11\0\1\u03b8\1\0\4\7\1\u03b9\1\7"+ + "\1\0\1\7\6\0\3\7\1\0\1\7\1\0\1\7"+ + "\1\0\15\7\2\0\2\7\11\0\1\7\1\0\6\7"+ + "\1\0\1\7\6\0\3\7\1\0\1\7\1\0\1\7"+ + "\1\0\3\7\1\u03ba\11\7\2\0\2\7\11\0\1\u03bb"+ + "\1\0\6\7\1\0\1\7\6\0\2\7\1\u03bc\1\0"+ + "\1\7\1\0\1\7\1\u0392\1\u03bd\14\7\2\0\2\7"+ + "\11\0\1\7\1\0\6\7\1\0\1\7\6\0\1\7"+ + "\1\u03be\1\7\1\0\1\7\1\0\1\7\1\0\1\7"+ + "\1\u03bf\13\7\2\0\2\7\11\0\1\u03c0\1\0\2\7"+ + "\1\u03c1\1\7\1\u03c2\1\7\1\0\1\7\6\0\2\7"+ + "\1\u03c3\1\0\1\7\1\0\1\7\1\0\15\7\2\0"+ + "\2\7\11\0\1\u03c4\1\0\1\u03c5\5\7\1\0\1\7"+ + "\6\0\3\7\1\0\1\7\1\0\1\7\1\u039b\1\u03c6"+ + "\1\u03c7\1\7\1\u03c8\1\u03c9\1\u03ca\7\7\2\0\2\7"+ + "\11\0\1\u03cb\1\0\6\7\1\0\1\7\6\0\2\7"+ + "\1\u03cc\1\0\1\7\1\0\1\7\1\u03a2\1\u03cd\14\7"+ + "\2\0\2\7\11\0\1\7\1\0\6\7\1\0\1\7"+ + "\6\0\1\u03ce\2\7\1\0\1\7\1\0\1\7\1\0"+ + "\3\7\1\u03cf\11\7\2\0\2\7\11\0\1\7\1\0"+ + "\6\7\1\u03a5\1\u03d0\6\0\1\u03d1\2\7\1\0\1\7"+ + "\1\0\1\7\1\0\15\7\2\0\2\7\11\0\1\7"+ + "\1\0\4\7\1\u03d2\1\u03d3\1\0\1\7\6\0\3\7"+ + "\1\0\1\7\1\0\1\7\1\0\4\7\1\u03d4\10\7"+ + "\2\0\2\7\11\0\1\u03d5\1\0\6\7\1\0\1\7"+ + "\6\0\2\7\1\u03d6\1\0\1\7\1\0\1\7\1\0"+ + "\15\7\2\0\2\7\11\0\1\7\1\0\1\7\1\u03d7"+ + "\1\u03d8\2\7\1\u03d9\1\0\1\7\6\0\2\7\1\u03da"+ + "\1\0\1\7\1\0\1\7\1\0\15\7\2\0\2\7"+ + "\11\0\1\7\1\0\6\7\1\0\1\7\6\0\2\7"+ + "\1\u03db\1\0\1\7\1\0\1\7\1\u03b1\1\u03dc\14\7"+ + "\2\0\2\7\11\0\1\7\1\0\1\7\1\u03dd\4\7"+ + "\1\0\1\7\6\0\3\7\1\0\1\7\1\0\1\7"+ + "\1\0\15\7\2\0\2\7\11\0\1\7\1\0\1\u0192"+ + "\5\7\1\0\1\7\6\0\3\7\1\0\1\7\1\0"+ + "\1\7\1\0\15\7\2\0\2\7\11\0\1\7\1\0"+ + "\6\7\1\0\1\7\6\0\3\7\1\0\1\u03de\1\0"+ + "\1\7\1\0\15\7\23\0\1\u028e\40\0\2\7\11\0"+ + "\1\7\1\0\4\7\1\u028f\1\7\1\0\1\7\6\0"+ + "\3\7\1\0\1\7\1\0\1\7\1\0\15\7\20\0"+ + "\1\u03df\43\0\2\7\11\0\1\7\1\0\1\7\1\u03e0"+ + "\4\7\1\0\1\7\6\0\3\7\1\0\1\7\1\0"+ + "\1\7\1\0\15\7\50\0\1\162\13\0\2\7\11\0"+ + "\1\7\1\0\6\7\1\0\1\7\6\0\3\7\1\0"+ + "\1\7\1\0\1\7\1\0\3\7\1\174\11\7\50\0"+ + "\1\u03e1\30\0\1\u03e2\44\0\2\7\11\0\1\7\1\0"+ + "\6\7\1\0\1\7\6\0\3\7\1\0\1\7\1\0"+ + "\1\7\1\0\3\7\1\u03e3\11\7\2\0\2\7\11\0"+ + "\1\7\1\0\1\u03e4\5\7\1\0\1\7\6\0\3\7"+ + "\1\0\1\7\1\0\1\7\1\0\15\7\44\0\2\113"+ + "\31\0\1\u03e5\46\0\2\7\11\0\1\u03e6\1\0\6\7"+ + "\1\0\1\7\6\0\3\7\1\0\1\7\1\0\1\7"+ + "\1\0\15\7\44\0\2\u03e7\33\0\1\u03e8\115\0\1\u03e9"+ + "\31\0\1\u03ea\100\0\2\u03eb\72\0\1\u03ec\3\0\1\u03ed"+ + "\36\0\1\u03ee\62\0\1\u03ef\42\0\1\u03f0\110\0\1\u03f1"+ + "\50\0\1\u03f2\62\0\1\u03f3\42\0\1\u03f4\120\0\1\u03f5"+ + "\26\0\1\u03f6\57\0\1\u03f7\77\0\1\u03f8\7\0\1\u024e"+ + "\31\0\1\u03f9\4\0\1\u03fa\10\0\1\u03fb\47\0\1\u03fc"+ + "\13\0\1\u03fd\71\0\1\u03fe\32\0\1\u03ff\2\0\1\u0400"+ + "\62\0\1\u0401\56\0\1\u0402\63\0\1\u0403\63\0\2\u0404"+ + "\55\0\1\u0405\64\0\2\u0406\50\0\1\u0407\2\0\1\u0408"+ + "\1\u0409\1\0\1\u040a\1\0\2\u040b\17\0\1\u040c\1\u040d"+ + "\1\u040e\26\0\1\u040f\110\0\2\u0410\2\0\1\u0411\3\0"+ + "\1\u0412\32\0\2\u0413\70\0\1\u0414\63\0\1\u0415\4\0"+ + "\2\u0416\31\0\1\u0417\5\0\1\u0418\55\0\1\u0419\101\0"+ + "\1\u041a\37\0\1\u041b\71\0\2\u041c\56\0\1\u041d\106\0"+ + "\1\u041e\67\0\1\u041f\52\0\1\u0420\56\0\2\u0421\16\0"+ + "\2\7\11\0\1\7\1\0\6\7\1\0\1\7\6\0"+ + "\3\7\1\0\1\7\1\0\1\7\1\u03e7\1\u0422\14\7"+ + "\2\0\2\7\11\0\1\7\1\0\1\u0423\5\7\1\0"+ + "\1\7\6\0\3\7\1\0\1\7\1\0\1\7\1\0"+ + "\15\7\2\0\2\7\11\0\1\7\1\0\6\7\1\0"+ + "\1\7\6\0\3\7\1\0\1\7\1\0\1\7\1\0"+ + "\6\7\1\u0424\6\7\2\0\2\7\11\0\1\7\1\0"+ + "\4\7\1\u0425\1\7\1\0\1\7\6\0\3\7\1\0"+ + "\1\7\1\0\1\7\1\0\15\7\2\0\2\7\11\0"+ + "\1\7\1\0\6\7\1\0\1\7\6\0\3\7\1\0"+ + "\1\7\1\u03eb\1\u0426\1\0\15\7\2\0\2\7\11\0"+ + "\1\7\1\0\6\7\1\0\1\7\6\0\3\7\1\0"+ + "\1\7\1\0\1\7\1\0\7\7\1\u0427\3\7\1\u0428"+ + "\1\7\2\0\2\7\11\0\1\7\1\0\6\7\1\0"+ + "\1\7\6\0\1\u0429\2\7\1\0\1\7\1\0\1\7"+ + "\1\0\15\7\2\0\2\7\11\0\1\7\1\0\6\7"+ + "\1\0\1\7\6\0\1\7\1\u042a\1\7\1\0\1\7"+ + "\1\0\1\7\1\0\15\7\2\0\2\7\11\0\1\7"+ + "\1\0\1\u042b\5\7\1\0\1\7\6\0\3\7\1\0"+ + "\1\7\1\0\1\7\1\0\15\7\2\0\2\7\11\0"+ + "\1\7\1\0\6\7\1\0\1\7\6\0\3\7\1\0"+ + "\1\7\1\0\1\7\1\0\1\7\1\u042c\13\7\2\0"+ + "\2\7\11\0\1\7\1\0\6\7\1\0\1\7\6\0"+ + "\1\u042d\2\7\1\0\1\7\1\0\1\7\1\0\15\7"+ + "\2\0\2\7\11\0\1\7\1\0\6\7\1\0\1\7"+ + "\6\0\1\7\1\u042e\1\7\1\0\1\7\1\0\1\7"+ + "\1\0\15\7\2\0\2\7\11\0\1\7\1\0\1\u042f"+ + "\5\7\1\0\1\7\6\0\3\7\1\0\1\7\1\0"+ + "\1\7\1\0\15\7\2\0\2\7\11\0\1\7\1\0"+ + "\6\7\1\0\1\7\6\0\3\7\1\0\1\7\1\0"+ + "\1\7\1\0\11\7\1\u0430\3\7\2\0\2\7\11\0"+ + "\1\7\1\0\4\7\1\u0431\1\7\1\0\1\7\6\0"+ + "\3\7\1\0\1\7\1\0\1\7\1\0\15\7\2\0"+ + "\2\7\11\0\1\7\1\0\2\7\1\u0432\3\7\1\0"+ + "\1\7\6\0\3\7\1\0\1\7\1\0\1\7\1\0"+ + "\15\7\2\0\2\7\11\0\1\7\1\0\6\7\1\0"+ + "\1\7\6\0\2\7\1\u0433\1\0\1\7\1\0\1\7"+ + "\1\0\2\7\1\273\12\7\2\0\2\7\11\0\1\7"+ + "\1\0\1\u0434\4\7\1\u0435\1\0\1\7\6\0\1\u0436"+ + "\2\7\1\0\1\7\1\0\1\7\1\0\15\7\2\0"+ + "\2\7\11\0\1\7\1\0\4\7\1\u0437\1\7\1\0"+ + "\1\7\6\0\2\7\1\u0438\1\0\1\7\1\0\1\7"+ + "\1\0\15\7\2\0\2\7\11\0\1\7\1\0\6\7"+ + "\1\0\1\7\6\0\3\7\1\0\1\7\1\0\1\7"+ + "\1\0\2\7\1\u0439\12\7\2\0\2\7\11\0\1\7"+ + "\1\0\1\7\1\u043a\2\7\1\u043b\1\7\1\0\1\7"+ + "\6\0\3\7\1\0\1\7\1\0\1\7\1\0\15\7"+ + "\2\0\2\7\11\0\1\7\1\0\5\7\1\u043c\1\0"+ + "\1\7\6\0\3\7\1\0\1\7\1\0\1\7\1\0"+ + "\15\7\2\0\2\7\11\0\1\7\1\0\2\7\1\u043d"+ + "\3\7\1\0\1\7\6\0\3\7\1\0\1\7\1\0"+ + "\1\7\1\0\15\7\2\0\2\7\11\0\1\7\1\0"+ + "\4\7\1\u043e\1\7\1\0\1\7\6\0\3\7\1\0"+ + "\1\7\1\0\1\7\1\0\15\7\2\0\2\7\11\0"+ + "\1\7\1\0\6\7\1\u0404\1\u043f\6\0\3\7\1\0"+ + "\1\7\1\0\1\7\1\0\15\7\2\0\2\7\11\0"+ + "\1\7\1\0\6\7\1\u0406\1\u0440\6\0\3\7\1\0"+ + "\1\7\1\0\1\7\1\0\15\7\2\0\2\7\11\0"+ + "\1\u0441\1\0\1\7\1\u0442\1\u0443\1\7\1\u0444\1\7"+ + "\1\u040b\1\u0445\6\0\3\7\1\0\1\7\1\0\1\7"+ + "\1\0\1\7\1\u0446\1\u0447\1\u0448\11\7\2\0\2\7"+ + "\11\0\1\u0449\1\0\6\7\1\0\1\7\6\0\3\7"+ + "\1\0\1\7\1\0\1\7\1\0\15\7\2\0\2\7"+ + "\11\0\1\7\1\0\6\7\1\0\1\7\6\0\3\7"+ + "\1\0\1\7\1\0\1\7\1\u0410\1\u044a\2\7\1\u044b"+ + "\3\7\1\u044c\5\7\2\0\2\7\11\0\1\7\1\0"+ + "\6\7\1\u0413\1\u044d\6\0\3\7\1\0\1\7\1\0"+ + "\1\7\1\0\15\7\2\0\2\7\11\0\1\7\1\0"+ + "\6\7\1\0\1\7\6\0\1\u044e\2\7\1\0\1\7"+ + "\1\0\1\7\1\0\15\7\2\0\2\7\11\0\1\7"+ + "\1\0\6\7\1\0\1\7\6\0\2\7\1\u044f\1\0"+ + "\1\7\1\0\1\7\1\u0416\1\u0450\14\7\2\0\2\7"+ + "\11\0\1\u0451\1\0\4\7\1\u0452\1\7\1\0\1\7"+ + "\6\0\3\7\1\0\1\7\1\0\1\7\1\0\15\7"+ + "\2\0\2\7\11\0\1\7\1\0\1\u0453\5\7\1\0"+ + "\1\7\6\0\3\7\1\0\1\7\1\0\1\7\1\0"+ + "\15\7\2\0\2\7\11\0\1\7\1\0\6\7\1\0"+ + "\1\7\6\0\2\7\1\u0454\1\0\1\7\1\0\1\7"+ + "\1\0\15\7\2\0\2\7\11\0\1\u0455\1\0\6\7"+ + "\1\0\1\7\6\0\3\7\1\0\1\7\1\0\1\7"+ + "\1\0\15\7\2\0\2\7\11\0\1\7\1\0\6\7"+ + "\1\u041c\1\u0456\6\0\3\7\1\0\1\7\1\0\1\7"+ + "\1\0\15\7\2\0\2\7\11\0\1\7\1\0\4\7"+ + "\1\u0457\1\7\1\0\1\7\6\0\3\7\1\0\1\7"+ + "\1\0\1\7\1\0\15\7\2\0\2\7\11\0\1\7"+ + "\1\0\6\7\1\0\1\7\6\0\3\7\1\0\1\7"+ + "\1\0\1\7\1\0\3\7\1\u0458\11\7\2\0\2\7"+ + "\11\0\1\7\1\0\6\7\1\0\1\7\6\0\3\7"+ + "\1\0\1\7\1\0\1\7\1\0\11\7\1\u0459\3\7"+ + "\2\0\2\7\11\0\1\7\1\0\6\7\1\0\1\7"+ + "\6\0\3\7\1\0\1\7\1\0\1\7\1\0\2\7"+ + "\1\u045a\12\7\2\0\2\7\11\0\1\7\1\0\6\7"+ + "\1\0\1\7\6\0\3\7\1\0\1\7\1\0\1\7"+ + "\1\u0421\1\u045b\14\7\2\0\2\7\11\0\1\7\1\0"+ + "\4\7\1\u045c\1\7\1\0\1\7\6\0\3\7\1\0"+ + "\1\7\1\0\1\7\1\0\15\7\37\0\1\u045d\24\0"+ + "\2\7\11\0\1\7\1\0\6\7\1\0\1\7\6\0"+ + "\2\7\1\u045e\1\0\1\7\1\0\1\7\1\0\15\7"+ + "\44\0\2\u045f\34\0\1\u0460\43\0\2\7\11\0\1\7"+ + "\1\0\6\7\1\0\1\7\6\0\3\7\1\0\1\7"+ + "\1\0\1\7\1\u045f\1\u0461\14\7\2\0\2\7\11\0"+ + "\1\7\1\0\1\7\1\u0462\4\7\1\0\1\7\6\0"+ + "\3\7\1\0\1\7\1\0\1\7\1\0\15\7\36\0"+ + "\1\u0463\25\0\2\7\11\0\1\7\1\0\6\7\1\0"+ + "\1\7\6\0\1\7\1\u0464\1\7\1\0\1\7\1\0"+ + "\1\7\1\0\15\7\35\0\1\u0465\41\0\1\u0466\63\0"+ + "\1\u0467\77\0\1\u0468\41\0\1\u0469\103\0\1\u046a\43\0"+ + "\1\u046b\76\0\1\u046c\46\0\1\u046d\104\0\1\u046e\34\0"+ + "\1\u046f\76\0\1\u0470\61\0\1\u0471\67\0\2\u0472\31\0"+ + "\1\u0473\112\0\1\u0474\50\0\1\u0475\66\0\2\u0476\35\0"+ + "\1\u0477\3\0\1\u0478\1\u0479\2\u047a\10\0\1\u047b\1\0"+ + "\1\u047c\2\u047d\2\u047e\1\u047f\1\u0480\1\0\1\u0481\2\0"+ + "\1\u0482\22\0\1\u0483\102\0\1\u0484\43\0\1\u0485\111\0"+ + "\1\u0486\46\0\1\u0487\63\0\1\u0488\46\0\1\u0489\101\0"+ + "\2\u048a\33\0\1\u048b\112\0\1\u048c\30\0\1\u048d\106\0"+ + "\2\u01bc\64\0\1\u048e\47\0\1\u048f\40\0\1\u0490\71\0"+ + "\2\u0491\71\0\1\u0492\62\0\1\u0493\6\0\1\u0494\1\0"+ + "\1\u0495\62\0\1\u0496\30\0\1\u0497\100\0\1\u0498\57\0"+ + "\1\u0499\43\0\1\u049a\66\0\1\u049b\52\0\1\u049c\63\0"+ + "\1\u049d\66\0\1\u049e\21\0\1\u049f\40\0\2\u04a0\101\0"+ + "\1\u04a1\61\0\1\u04a2\50\0\1\u04a3\43\0\1\u04a4\1\0"+ + "\1\u04a5\1\u04a6\1\0\1\u04a7\2\u04a8\6\0\1\u04a9\1\0"+ + "\1\u04aa\4\0\2\u04ab\1\u04ac\1\u04ad\1\u04ae\3\0\1\u04af"+ + "\54\0\1\u04b0\37\0\2\u04b1\104\0\1\u041e\65\0\1\u04b2"+ + "\60\0\1\u04b3\22\0\1\u04b4\115\0\1\u04b5\27\0\1\u04b6"+ + "\44\0\2\7\11\0\1\7\1\0\6\7\1\0\1\7"+ + "\6\0\1\u04b7\2\7\1\0\1\7\1\0\1\7\1\0"+ + "\15\7\2\0\2\7\11\0\1\u04b8\1\0\6\7\1\0"+ + "\1\7\6\0\3\7\1\0\1\7\1\0\1\7\1\0"+ + "\15\7\2\0\2\7\11\0\1\7\1\0\1\u04b9\5\7"+ + "\1\0\1\7\6\0\3\7\1\0\1\7\1\0\1\7"+ + "\1\0\15\7\2\0\2\7\11\0\1\7\1\0\6\7"+ + "\1\0\1\7\6\0\1\u04ba\2\7\1\0\1\7\1\0"+ + "\1\7\1\0\15\7\2\0\2\7\11\0\1\u04bb\1\0"+ + "\6\7\1\0\1\7\6\0\3\7\1\0\1\7\1\0"+ + "\1\7\1\0\15\7\2\0\2\7\11\0\1\7\1\0"+ + "\6\7\1\0\1\7\6\0\2\7\1\u04bc\1\0\1\7"+ + "\1\0\1\7\1\0\15\7\2\0\2\7\11\0\1\7"+ + "\1\0\2\7\1\u04bd\3\7\1\0\1\7\6\0\3\7"+ + "\1\0\1\7\1\0\1\7\1\0\15\7\2\0\2\7"+ + "\11\0\1\7\1\0\6\7\1\0\1\7\6\0\1\7"+ + "\1\u04be\1\7\1\0\1\7\1\0\1\7\1\0\15\7"+ + "\2\0\2\7\11\0\1\7\1\0\4\7\1\u04bf\1\7"+ + "\1\0\1\7\6\0\3\7\1\0\1\7\1\0\1\7"+ + "\1\0\15\7\2\0\2\7\11\0\1\7\1\0\6\7"+ + "\1\0\1\7\6\0\3\7\1\0\1\7\1\0\1\7"+ + "\1\0\1\7\1\u04c0\13\7\2\0\2\7\11\0\1\7"+ + "\1\0\2\7\1\u04c1\3\7\1\0\1\7\6\0\3\7"+ + "\1\0\1\7\1\0\1\7\1\0\15\7\2\0\2\7"+ + "\11\0\1\7\1\0\6\7\1\0\1\7\6\0\1\7"+ + "\1\u04c2\1\7\1\0\1\7\1\0\1\7\1\0\15\7"+ + "\2\0\2\7\11\0\1\7\1\0\6\7\1\0\1\7"+ + "\6\0\1\7\1\u04c3\1\7\1\0\1\7\1\0\1\7"+ + "\1\0\15\7\2\0\2\7\11\0\1\7\1\0\6\7"+ + "\1\0\1\7\6\0\3\7\1\0\1\7\1\0\1\7"+ + "\1\u0472\1\u04c4\14\7\2\0\2\7\11\0\1\u04c5\1\0"+ + "\6\7\1\0\1\7\6\0\3\7\1\0\1\7\1\0"+ + "\1\7\1\0\15\7\2\0\2\7\11\0\1\7\1\0"+ + "\6\7\1\0\1\7\6\0\3\7\1\0\1\7\1\0"+ + "\1\7\1\0\1\7\1\u04c6\13\7\2\0\2\7\11\0"+ + "\1\7\1\0\6\7\1\0\1\7\6\0\1\u04c7\2\7"+ + "\1\0\1\7\1\0\1\7\1\0\15\7\2\0\2\7"+ + "\11\0\1\7\1\0\6\7\1\0\1\7\6\0\3\7"+ + "\1\0\1\7\1\u0476\1\u04c8\1\0\15\7\2\0\2\7"+ + "\11\0\1\7\1\0\1\u04c9\3\7\1\u04ca\1\u04cb\1\u047a"+ + "\1\u04cc\6\0\2\7\1\u04cd\1\0\1\u04ce\1\u047d\1\u04cf"+ + "\1\u047e\1\u04d0\1\u04d1\1\u04d2\1\7\1\u04d3\2\7\1\u04d4"+ + "\5\7\2\0\2\7\11\0\1\u04d5\1\0\6\7\1\0"+ + "\1\7\6\0\3\7\1\0\1\7\1\0\1\7\1\0"+ + "\15\7\2\0\2\7\11\0\1\7\1\0\6\7\1\0"+ + "\1\7\6\0\1\7\1\u04d6\1\7\1\0\1\7\1\0"+ + "\1\7\1\0\15\7\2\0\2\7\11\0\1\7\1\0"+ + "\1\7\1\u04d7\4\7\1\0\1\7\6\0\3\7\1\0"+ + "\1\7\1\0\1\7\1\0\15\7\2\0\2\7\11\0"+ + "\1\7\1\0\6\7\1\0\1\7\6\0\3\7\1\0"+ + "\1\7\1\0\1\7\1\0\3\7\1\u04d8\11\7\2\0"+ + "\2\7\11\0\1\7\1\0\6\7\1\0\1\7\6\0"+ + "\1\u04d9\2\7\1\0\1\7\1\0\1\7\1\0\15\7"+ + "\2\0\2\7\11\0\1\7\1\0\6\7\1\0\1\7"+ + "\6\0\2\7\1\u04da\1\0\1\7\1\0\1\7\1\0"+ + "\15\7\2\0\2\7\11\0\1\7\1\0\5\7\1\u04db"+ + "\1\0\1\7\6\0\3\7\1\0\1\7\1\0\1\7"+ + "\1\0\15\7\2\0\2\7\11\0\1\7\1\0\6\7"+ + "\1\0\1\7\6\0\3\7\1\0\1\7\1\0\1\7"+ + "\1\u048a\1\u04dc\14\7\2\0\2\7\11\0\1\7\1\0"+ + "\1\u04dd\5\7\1\0\1\7\6\0\3\7\1\0\1\7"+ + "\1\0\1\7\1\0\15\7\2\0\2\7\11\0\1\7"+ + "\1\0\6\7\1\0\1\7\6\0\3\7\1\0\1\7"+ + "\1\0\1\7\1\0\3\7\1\u04de\11\7\2\0\2\7"+ + "\11\0\1\7\1\0\1\u04df\5\7\1\0\1\7\6\0"+ + "\3\7\1\0\1\7\1\0\1\7\1\0\15\7\2\0"+ + "\2\7\11\0\1\7\1\0\6\7\1\0\1\7\6\0"+ + "\3\7\1\0\1\7\1\0\1\7\1\0\3\7\1\u04e0"+ + "\11\7\2\0\2\7\11\0\1\7\1\0\6\7\1\0"+ + "\1\7\6\0\1\7\1\u04e1\1\7\1\0\1\7\1\0"+ + "\1\7\1\0\15\7\2\0\2\7\11\0\1\u04e2\1\0"+ + "\6\7\1\0\1\7\6\0\3\7\1\0\1\7\1\0"+ + "\1\7\1\0\15\7\2\0\2\7\11\0\1\7\1\0"+ + "\6\7\1\u0491\1\u04e3\6\0\3\7\1\0\1\7\1\0"+ + "\1\7\1\0\15\7\2\0\2\7\11\0\1\7\1\0"+ + "\6\7\1\0\1\7\6\0\1\7\1\u04e4\1\7\1\0"+ + "\1\7\1\0\1\7\1\0\15\7\2\0\2\7\11\0"+ + "\1\7\1\0\6\7\1\0\1\7\6\0\2\7\1\u04e5"+ + "\1\0\1\7\1\0\1\7\1\0\1\7\1\u04e6\1\7"+ + "\1\u04e7\11\7\2\0\2\7\11\0\1\7\1\0\6\7"+ + "\1\0\1\7\6\0\3\7\1\0\1\7\1\0\1\7"+ + "\1\0\4\7\1\u04e8\10\7\2\0\2\7\11\0\1\7"+ + "\1\0\1\7\1\u04e9\4\7\1\0\1\7\6\0\3\7"+ + "\1\0\1\7\1\0\1\7\1\0\15\7\2\0\2\7"+ + "\11\0\1\7\1\0\6\7\1\0\1\7\6\0\2\7"+ + "\1\u04ea\1\0\1\7\1\0\1\7\1\0\15\7\2\0"+ + "\2\7\11\0\1\7\1\0\6\7\1\0\1\7\6\0"+ + "\1\u04eb\2\7\1\0\1\7\1\0\1\7\1\0\15\7"+ + "\2\0\2\7\11\0\1\7\1\0\1\u04ec\5\7\1\0"+ + "\1\7\6\0\3\7\1\0\1\7\1\0\1\7\1\0"+ + "\15\7\2\0\2\7\11\0\1\7\1\0\5\7\1\u04ed"+ + "\1\0\1\7\6\0\3\7\1\0\1\7\1\0\1\7"+ + "\1\0\15\7\2\0\2\7\11\0\1\u04ee\1\0\6\7"+ + "\1\0\1\7\6\0\3\7\1\0\1\7\1\0\1\7"+ + "\1\0\15\7\2\0\2\7\11\0\1\7\1\0\1\u04ef"+ + "\5\7\1\0\1\7\6\0\3\7\1\0\1\7\1\0"+ + "\1\7\1\0\15\7\2\0\2\7\11\0\1\7\1\0"+ + "\5\7\1\u04f0\1\0\1\7\6\0\3\7\1\0\1\7"+ + "\1\0\1\7\1\0\1\7\1\u04f1\13\7\2\0\2\7"+ + "\11\0\1\7\1\0\6\7\1\u04a0\1\u04f2\6\0\3\7"+ + "\1\0\1\7\1\0\1\7\1\0\15\7\2\0\2\7"+ + "\11\0\1\7\1\0\6\7\1\0\1\7\6\0\3\7"+ + "\1\0\1\7\1\0\1\7\1\0\1\7\1\u04f3\13\7"+ + "\2\0\2\7\11\0\1\7\1\0\6\7\1\0\1\7"+ + "\6\0\3\7\1\0\1\7\1\0\1\7\1\0\1\7"+ + "\1\u04f4\13\7\2\0\2\7\11\0\1\7\1\0\6\7"+ + "\1\0\1\7\6\0\1\u04f5\2\7\1\0\1\7\1\0"+ + "\1\7\1\0\15\7\2\0\2\7\11\0\1\7\1\0"+ + "\1\u04f6\1\7\1\u04f7\1\u04f8\1\7\1\u04f9\1\u04a8\1\u04fa"+ + "\6\0\1\u04fb\1\7\1\u04fc\1\0\1\7\1\0\1\7"+ + "\1\u04ab\1\u04fd\1\u04fe\1\u04ff\1\u0500\3\7\1\u0501\5\7"+ + "\2\0\2\7\11\0\1\7\1\0\6\7\1\0\1\7"+ + "\6\0\3\7\1\0\1\7\1\0\1\7\1\0\2\7"+ + "\1\u0502\12\7\2\0\2\7\11\0\1\7\1\0\6\7"+ + "\1\u04b1\1\u0503\6\0\3\7\1\0\1\7\1\0\1\7"+ + "\1\0\15\7\2\0\2\7\11\0\1\7\1\0\6\7"+ + "\1\0\1\7\6\0\3\7\1\0\1\7\1\0\1\7"+ + "\1\0\4\7\1\u0458\10\7\2\0\2\7\11\0\1\7"+ + "\1\0\6\7\1\0\1\7\6\0\3\7\1\0\1\7"+ + "\1\0\1\7\1\0\10\7\1\u0504\4\7\2\0\2\7"+ + "\11\0\1\7\1\0\6\7\1\0\1\7\6\0\3\7"+ + "\1\0\1\7\1\0\1\7\1\0\7\7\1\u0505\5\7"+ + "\2\0\2\7\11\0\1\u0506\1\0\6\7\1\0\1\7"+ + "\6\0\3\7\1\0\1\7\1\0\1\7\1\0\15\7"+ + "\2\0\2\7\11\0\1\7\1\0\6\7\1\0\1\7"+ + "\6\0\3\7\1\0\1\7\1\0\1\7\1\0\4\7"+ + "\1\u0507\10\7\2\0\2\7\11\0\1\7\1\0\1\u0508"+ + "\5\7\1\0\1\7\6\0\3\7\1\0\1\7\1\0"+ + "\1\7\1\0\15\7\2\0\2\7\11\0\1\7\1\0"+ + "\6\7\1\0\1\7\6\0\3\7\1\0\1\7\1\0"+ + "\1\7\1\0\2\7\1\326\12\7\21\0\1\u023e\42\0"+ + "\2\7\11\0\1\7\1\0\2\7\1\367\3\7\1\0"+ + "\1\7\6\0\3\7\1\0\1\7\1\0\1\7\1\0"+ + "\15\7\35\0\1\u0335\47\0\1\u0354\40\0\2\7\11\0"+ + "\1\7\1\0\6\7\1\0\1\7\6\0\1\u0337\2\7"+ + "\1\0\1\7\1\0\1\7\1\0\15\7\2\0\2\7"+ + "\11\0\1\7\1\0\4\7\1\u0355\1\7\1\0\1\7"+ + "\6\0\3\7\1\0\1\7\1\0\1\7\1\0\15\7"+ + "\20\0\1\u0509\43\0\2\7\11\0\1\7\1\0\1\7"+ + "\1\u050a\4\7\1\0\1\7\6\0\3\7\1\0\1\7"+ + "\1\0\1\7\1\0\15\7\54\0\1\u050b\25\0\1\u050c"+ + "\56\0\1\u050d\71\0\2\u050e\60\0\2\u050f\111\0\1\u0510"+ + "\20\0\1\u0511\103\0\1\u0512\41\0\1\u0513\113\0\1\u0514"+ + "\35\0\2\u0515\54\0\1\u0516\22\0\2\u0517\40\0\1\u0518"+ + "\113\0\1\u0519\27\0\1\u051a\74\0\1\u051b\70\0\1\u051c"+ + "\36\0\1\u051d\75\0\1\u051e\42\0\1\u051f\64\0\1\u0520"+ + "\100\0\2\u0521\2\0\1\u0522\35\0\1\u0523\33\0\1\u0524"+ + "\26\0\1\u0525\53\0\1\u0526\101\0\1\u0527\50\0\1\u0528"+ + "\74\0\1\u0529\45\0\1\u052a\20\0\2\u052b\37\0\1\u052c"+ + "\20\0\2\u052d\62\0\1\u052e\61\0\1\u052f\32\0\1\u0530"+ + "\104\0\2\u0531\2\u0532\6\0\1\u0533\51\0\2\u0534\40\0"+ + "\1\u0535\52\0\1\u0536\63\0\1\u0537\100\0\1\u0538\50\0"+ + "\2\u0539\23\0\1\u053a\27\0\1\u053b\65\0\1\u053c\101\0"+ + "\2\u053d\70\0\1\u053e\22\0\1\u053f\102\0\1\u0540\44\0"+ + "\1\u0541\60\0\1\u0542\56\0\1\u0543\67\0\1\u0544\75\0"+ + "\1\u0545\42\0\1\u0546\105\0\2\u0547\31\0\1\u0548\67\0"+ + "\1\u0549\73\0\1\u054a\47\0\1\u054b\53\0\1\u0395\70\0"+ + "\1\u054c\52\0\1\u054d\106\0\2\u054e\60\0\2\u054f\65\0"+ + "\1\u0550\32\0\1\u0551\23\0\2\u0552\41\0\2\u0553\53\0"+ + "\1\u0554\23\0\2\u0555\60\0\2\u0556\31\0\1\u0557\30\0"+ + "\1\u0558\1\0\1\u0559\32\0\1\u055a\62\0\1\u0523\25\0"+ + "\1\u055b\46\0\1\u055c\45\0\1\u055d\2\0\1\u055e\12\0"+ + "\1\u055f\42\0\1\u0560\2\0\1\u0561\61\0\1\u0562\13\0"+ + "\1\u0563\37\0\1\u0564\5\0\1\u0565\13\0\1\u0566\46\0"+ + "\1\u0567\62\0\2\u0568\60\0\2\u0569\25\0\1\u0533\22\0"+ + "\1\u056a\64\0\1\u056b\65\0\1\u056c\52\0\1\u056d\46\0"+ + "\2\7\11\0\1\7\1\0\6\7\1\0\1\7\6\0"+ + "\3\7\1\0\1\7\1\0\1\7\1\0\7\7\1\u056e"+ + "\5\7\2\0\2\7\11\0\1\7\1\0\1\7\1\u056f"+ + "\4\7\1\0\1\7\6\0\3\7\1\0\1\7\1\0"+ + "\1\7\1\0\15\7\2\0\2\7\11\0\1\u0570\1\0"+ + "\6\7\1\0\1\7\6\0\3\7\1\0\1\7\1\0"+ + "\1\7\1\0\15\7\2\0\2\7\11\0\1\7\1\0"+ + "\6\7\1\u050e\1\u0571\6\0\3\7\1\0\1\7\1\0"+ + "\1\7\1\0\15\7\2\0\2\7\11\0\1\7\1\0"+ + "\6\7\1\u050f\1\u0572\6\0\3\7\1\0\1\7\1\0"+ + "\1\7\1\0\15\7\2\0\2\7\11\0\1\7\1\0"+ + "\6\7\1\0\1\7\6\0\3\7\1\0\1\7\1\0"+ + "\1\7\1\0\11\7\1\u0573\3\7\2\0\2\7\11\0"+ + "\1\u0574\1\0\6\7\1\0\1\7\6\0\3\7\1\0"+ + "\1\7\1\0\1\7\1\0\15\7\2\0\2\7\11\0"+ + "\1\7\1\0\6\7\1\0\1\7\6\0\2\7\1\u0575"+ + "\1\0\1\7\1\0\1\7\1\0\15\7\2\0\2\7"+ + "\11\0\1\7\1\0\1\u0576\5\7\1\0\1\7\6\0"+ + "\3\7\1\0\1\7\1\0\1\7\1\0\15\7\2\0"+ + "\2\7\11\0\1\7\1\0\6\7\1\0\1\7\6\0"+ + "\3\7\1\0\1\7\1\0\1\7\1\0\4\7\1\u0577"+ + "\10\7\2\0\2\7\11\0\1\7\1\0\6\7\1\u0515"+ + "\1\u0578\6\0\3\7\1\0\1\7\1\0\1\7\1\0"+ + "\15\7\2\0\2\7\11\0\1\7\1\0\2\7\1\u0579"+ + "\3\7\1\0\1\7\6\0\3\7\1\0\1\7\1\0"+ + "\1\7\1\u0517\1\u057a\14\7\2\0\2\7\11\0\1\7"+ + "\1\0\5\7\1\u057b\1\0\1\7\6\0\3\7\1\0"+ + "\1\7\1\0\1\7\1\0\15\7\2\0\2\7\11\0"+ + "\1\7\1\0\6\7\1\0\1\7\6\0\3\7\1\0"+ + "\1\7\1\0\1\7\1\0\11\7\1\u057c\3\7\2\0"+ + "\2\7\11\0\1\7\1\0\5\7\1\u057d\1\0\1\7"+ + "\6\0\3\7\1\0\1\7\1\0\1\7\1\0\15\7"+ + "\2\0\2\7\11\0\1\7\1\0\6\7\1\0\1\7"+ + "\6\0\2\7\1\u057e\1\0\1\7\1\0\1\7\1\0"+ + "\15\7\2\0\2\7\11\0\1\7\1\0\6\7\1\0"+ + "\1\7\6\0\3\7\1\0\1\7\1\0\1\7\1\0"+ + "\1\7\1\u057f\13\7\2\0\2\7\11\0\1\7\1\0"+ + "\4\7\1\u0580\1\7\1\0\1\7\6\0\3\7\1\0"+ + "\1\7\1\0\1\7\1\0\15\7\2\0\2\7\11\0"+ + "\1\7\1\0\6\7\1\0\1\7\6\0\2\7\1\u0581"+ + "\1\0\1\7\1\0\1\7\1\0\15\7\2\0\2\7"+ + "\11\0\1\7\1\0\1\7\1\u0582\4\7\1\0\1\7"+ + "\6\0\3\7\1\0\1\7\1\0\1\7\1\0\15\7"+ + "\2\0\2\7\11\0\1\7\1\0\4\7\1\u0583\1\7"+ + "\1\0\1\7\6\0\3\7\1\0\1\7\1\0\1\7"+ + "\1\0\15\7\2\0\2\7\11\0\1\7\1\0\6\7"+ + "\1\0\1\7\6\0\3\7\1\0\1\7\1\u0521\1\u0584"+ + "\1\0\1\7\1\u0585\13\7\2\0\2\7\11\0\1\7"+ + "\1\0\3\7\1\u0586\2\7\1\0\1\7\6\0\3\7"+ + "\1\0\1\7\1\0\1\7\1\0\11\7\1\u0587\3\7"+ + "\2\0\2\7\11\0\1\7\1\0\4\7\1\u0588\1\7"+ + "\1\0\1\7\6\0\3\7\1\0\1\7\1\0\1\7"+ + "\1\0\15\7\2\0\2\7\11\0\1\u0589\1\0\6\7"+ + "\1\0\1\7\6\0\3\7\1\0\1\7\1\0\1\7"+ + "\1\0\15\7\2\0\2\7\11\0\1\7\1\0\6\7"+ + "\1\0\1\7\6\0\1\u058a\2\7\1\0\1\7\1\0"+ + "\1\7\1\0\15\7\2\0\2\7\11\0\1\7\1\0"+ + "\5\7\1\u058b\1\0\1\7\6\0\3\7\1\0\1\7"+ + "\1\0\1\7\1\0\15\7\2\0\2\7\11\0\1\7"+ + "\1\0\6\7\1\0\1\7\6\0\2\7\1\u058c\1\0"+ + "\1\7\1\0\1\7\1\0\15\7\2\0\2\7\11\0"+ + "\1\7\1\0\4\7\1\u058d\1\7\1\0\1\7\6\0"+ + "\3\7\1\0\1\7\1\0\1\7\1\u052b\1\u058e\14\7"+ + "\2\0\2\7\11\0\1\7\1\0\4\7\1\u058f\1\7"+ + "\1\0\1\7\6\0\3\7\1\0\1\7\1\0\1\7"+ + "\1\u052d\1\u0590\14\7\2\0\2\7\11\0\1\7\1\0"+ + "\6\7\1\0\1\7\6\0\3\7\1\0\1\7\1\0"+ + "\1\7\1\0\1\7\1\u0591\13\7\2\0\2\7\11\0"+ + "\1\7\1\0\6\7\1\0\1\7\6\0\3\7\1\0"+ + "\1\7\1\0\1\7\1\0\1\7\1\u0592\13\7\2\0"+ + "\2\7\11\0\1\7\1\0\1\u0593\5\7\1\0\1\7"+ + "\6\0\3\7\1\0\1\7\1\0\1\7\1\0\15\7"+ + "\2\0\2\7\11\0\1\7\1\0\6\7\1\0\1\7"+ + "\6\0\3\7\1\0\1\7\1\u0531\1\u0594\1\u0532\1\u0595"+ + "\6\7\1\u0596\5\7\2\0\2\7\11\0\1\7\1\0"+ + "\6\7\1\0\1\7\6\0\3\7\1\0\1\7\1\0"+ + "\1\7\1\u0534\1\u0597\14\7\2\0\2\7\11\0\1\7"+ + "\1\0\5\7\1\u0598\1\0\1\7\6\0\3\7\1\0"+ + "\1\7\1\0\1\7\1\0\15\7\2\0\2\7\11\0"+ + "\1\u0599\1\0\6\7\1\0\1\7\6\0\3\7\1\0"+ + "\1\7\1\0\1\7\1\0\15\7\2\0\2\7\11\0"+ + "\1\7\1\0\1\u059a\5\7\1\0\1\7\6\0\3\7"+ + "\1\0\1\7\1\0\1\7\1\0\15\7\2\0\2\7"+ + "\11\0\1\7\1\0\6\7\1\0\1\7\6\0\1\7"+ + "\1\u059b\1\7\1\0\1\7\1\0\1\7\1\0\15\7"+ + "\2\0\2\7\11\0\1\7\1\0\6\7\1\u0539\1\u059c"+ + "\6\0\3\7\1\0\1\7\1\0\1\7\1\0\5\7"+ + "\1\u059d\7\7\2\0\2\7\11\0\1\7\1\0\1\7"+ + "\1\u059e\4\7\1\0\1\7\6\0\3\7\1\0\1\7"+ + "\1\0\1\7\1\0\15\7\2\0\2\7\11\0\1\7"+ + "\1\0\5\7\1\u059f\1\0\1\7\6\0\3\7\1\0"+ + "\1\7\1\0\1\7\1\0\15\7\2\0\2\7\11\0"+ + "\1\7\1\0\6\7\1\0\1\7\6\0\3\7\1\0"+ + "\1\7\1\0\1\7\1\u053d\1\u05a0\14\7\2\0\2\7"+ + "\11\0\1\7\1\0\6\7\1\0\1\7\6\0\3\7"+ + "\1\0\1\7\1\0\1\7\1\0\7\7\1\u05a1\5\7"+ + "\2\0\2\7\11\0\1\u05a2\1\0\6\7\1\0\1\7"+ + "\6\0\3\7\1\0\1\7\1\0\1\7\1\0\15\7"+ + "\2\0\2\7\11\0\1\7\1\0\6\7\1\0\1\7"+ + "\6\0\1\7\1\u05a3\1\7\1\0\1\7\1\0\1\7"+ + "\1\0\15\7\2\0\2\7\11\0\1\7\1\0\2\7"+ + "\1\u05a4\3\7\1\0\1\7\6\0\3\7\1\0\1\7"+ + "\1\0\1\7\1\0\15\7\2\0\2\7\11\0\1\7"+ + "\1\0\1\7\1\u05a5\4\7\1\0\1\7\6\0\3\7"+ + "\1\0\1\7\1\0\1\7\1\0\15\7\2\0\2\7"+ + "\11\0\1\u05a6\1\0\6\7\1\0\1\7\6\0\3\7"+ + "\1\0\1\7\1\0\1\7\1\0\15\7\2\0\2\7"+ + "\11\0\1\7\1\0\4\7\1\u05a7\1\7\1\0\1\7"+ + "\6\0\3\7\1\0\1\7\1\0\1\7\1\0\15\7"+ + "\2\0\2\7\11\0\1\7\1\0\6\7\1\0\1\7"+ + "\6\0\2\7\1\u05a8\1\0\1\7\1\0\1\7\1\0"+ + "\15\7\2\0\2\7\11\0\1\7\1\0\1\7\1\u05a9"+ + "\4\7\1\0\1\7\6\0\3\7\1\0\1\7\1\0"+ + "\1\7\1\0\15\7\2\0\2\7\11\0\1\7\1\0"+ + "\6\7\1\0\1\7\6\0\3\7\1\0\1\7\1\0"+ + "\1\7\1\u0547\1\u05aa\14\7\2\0\2\7\11\0\1\u05ab"+ + "\1\0\6\7\1\0\1\7\6\0\3\7\1\0\1\7"+ + "\1\0\1\7\1\0\15\7\2\0\2\7\11\0\1\7"+ + "\1\0\4\7\1\u05ac\1\7\1\0\1\7\6\0\3\7"+ + "\1\0\1\7\1\0\1\7\1\0\15\7\2\0\2\7"+ + "\11\0\1\7\1\0\6\7\1\0\1\7\6\0\1\u05ad"+ + "\2\7\1\0\1\7\1\0\1\7\1\0\15\7\2\0"+ + "\2\7\11\0\1\7\1\0\4\7\1\u05ae\1\7\1\0"+ + "\1\7\6\0\3\7\1\0\1\7\1\0\1\7\1\0"+ + "\15\7\2\0\2\7\11\0\1\u03c0\1\0\6\7\1\0"+ + "\1\7\6\0\3\7\1\0\1\7\1\0\1\7\1\0"+ + "\15\7\2\0\2\7\11\0\1\7\1\0\5\7\1\u05af"+ + "\1\0\1\7\6\0\3\7\1\0\1\7\1\0\1\7"+ + "\1\0\15\7\2\0\2\7\11\0\1\u05b0\1\0\6\7"+ + "\1\0\1\7\6\0\3\7\1\0\1\7\1\0\1\7"+ + "\1\0\15\7\2\0\2\7\11\0\1\7\1\0\6\7"+ + "\1\0\1\7\6\0\3\7\1\0\1\7\1\u054e\1\u05b1"+ + "\1\0\15\7\2\0\2\7\11\0\1\7\1\0\6\7"+ + "\1\0\1\7\6\0\3\7\1\0\1\7\1\u054f\1\u05b2"+ + "\1\0\15\7\2\0\2\7\11\0\1\7\1\0\6\7"+ + "\1\0\1\7\6\0\3\7\1\0\1\7\1\0\1\7"+ + "\1\0\2\7\1\u05b3\12\7\2\0\2\7\11\0\1\7"+ + "\1\0\1\7\1\u05b4\4\7\1\0\1\7\6\0\3\7"+ + "\1\0\1\7\1\0\1\7\1\u0552\1\u05b5\14\7\2\0"+ + "\2\7\11\0\1\7\1\0\6\7\1\u0553\1\u05b6\6\0"+ + "\3\7\1\0\1\7\1\0\1\7\1\0\15\7\2\0"+ + "\2\7\11\0\1\7\1\0\1\7\1\u05b7\4\7\1\0"+ + "\1\7\6\0\3\7\1\0\1\7\1\0\1\7\1\u0555"+ + "\1\u05b8\14\7\2\0\2\7\11\0\1\7\1\0\6\7"+ + "\1\0\1\7\6\0\3\7\1\0\1\7\1\0\1\7"+ + "\1\u0556\1\u05b9\14\7\2\0\2\7\11\0\1\u05ba\1\0"+ + "\6\7\1\0\1\7\6\0\3\7\1\0\1\7\1\0"+ + "\1\7\1\0\1\7\1\u05bb\1\7\1\u05bc\11\7\2\0"+ + "\2\7\11\0\1\7\1\0\2\7\1\u05bd\3\7\1\0"+ + "\1\7\6\0\3\7\1\0\1\7\1\0\1\7\1\0"+ + "\15\7\2\0\2\7\11\0\1\7\1\0\3\7\1\u0586"+ + "\2\7\1\0\1\7\6\0\3\7\1\0\1\7\1\0"+ + "\1\7\1\0\3\7\1\u05be\11\7\2\0\2\7\11\0"+ + "\1\7\1\0\6\7\1\0\1\7\6\0\1\u05bf\2\7"+ + "\1\0\1\7\1\0\1\7\1\0\15\7\2\0\2\7"+ + "\11\0\1\7\1\0\2\7\1\u05c0\2\7\1\u05c1\1\0"+ + "\1\7\6\0\2\7\1\u05c2\1\0\1\7\1\0\1\7"+ + "\1\0\15\7\2\0\2\7\11\0\1\7\1\0\1\7"+ + "\1\u05c3\2\7\1\u05c4\1\7\1\0\1\7\6\0\3\7"+ + "\1\0\1\7\1\0\1\7\1\0\15\7\2\0\2\7"+ + "\11\0\1\7\1\0\4\7\1\u05c5\1\7\1\0\1\7"+ + "\6\0\2\7\1\u05c6\1\0\1\7\1\0\1\7\1\0"+ + "\15\7\2\0\2\7\11\0\1\u05c7\1\0\4\7\1\u05c8"+ + "\1\7\1\0\1\7\6\0\2\7\1\u05c9\1\0\1\7"+ + "\1\0\1\7\1\0\15\7\2\0\2\7\11\0\1\7"+ + "\1\0\5\7\1\u05ca\1\0\1\7\6\0\3\7\1\0"+ + "\1\7\1\0\1\7\1\0\15\7\2\0\2\7\11\0"+ + "\1\7\1\0\6\7\1\u0568\1\u05cb\6\0\3\7\1\0"+ + "\1\7\1\0\1\7\1\0\15\7\2\0\2\7\11\0"+ + "\1\7\1\0\6\7\1\u0569\1\u05cc\6\0\3\7\1\0"+ + "\1\7\1\0\1\7\1\0\7\7\1\u0596\5\7\2\0"+ + "\2\7\11\0\1\u05cd\1\0\6\7\1\0\1\7\6\0"+ + "\3\7\1\0\1\7\1\0\1\7\1\0\15\7\2\0"+ + "\2\7\11\0\1\7\1\0\1\7\1\u05ce\4\7\1\0"+ + "\1\7\6\0\3\7\1\0\1\7\1\0\1\7\1\0"+ + "\15\7\2\0\2\7\11\0\1\7\1\0\5\7\1\u05cf"+ + "\1\0\1\7\6\0\3\7\1\0\1\7\1\0\1\7"+ + "\1\0\15\7\2\0\2\7\11\0\1\u05d0\1\0\6\7"+ + "\1\0\1\7\6\0\3\7\1\0\1\7\1\0\1\7"+ + "\1\0\15\7\23\0\1\u0335\13\0\1\u023e\24\0\2\7"+ + "\11\0\1\7\1\0\4\7\1\u0337\1\7\1\0\1\7"+ + "\6\0\2\7\1\367\1\0\1\7\1\0\1\7\1\0"+ + "\15\7\21\0\1\u05d1\55\0\1\u05d2\102\0\1\u05d3\67\0"+ + "\2\u05d4\62\0\1\u05d5\30\0\1\u05d6\71\0\2\u05d7\107\0"+ + "\1\u05d8\22\0\1\u05d9\71\0\2\317\52\0\1\u05da\106\0"+ + "\2\u05db\33\0\1\u05dc\110\0\1\u05dd\36\0\1\u05de\57\0"+ + "\1\u05df\75\0\1\u05e0\75\0\1\u05e1\27\0\1\u05e2\104\0"+ + "\2\u05e3\33\0\1\u05e4\101\0\1\u05e5\70\0\2\u05e6\34\0"+ + "\1\u05e7\63\0\1\u05e8\54\0\1\u05e9\112\0\1\u05ea\70\0"+ + "\1\u024e\50\0\2\u05eb\53\0\1\u05ec\61\0\1\u05ed\66\0"+ + "\2\u05ee\63\0\1\u05ef\14\0\1\u05f0\114\0\1\u05f0\43\0"+ + "\1\u05f1\113\0\1\u05f2\52\0\2\u0531\2\u0532\31\0\1\u05f3"+ + "\63\0\1\u05f4\101\0\1\u05f5\57\0\1\u05f6\50\0\1\u05f7"+ + "\54\0\1\u05f8\106\0\2\u05f9\53\0\1\u05fa\46\0\1\u05fb"+ + "\101\0\2\u05fc\53\0\1\u05fd\45\0\1\u05fe\55\0\1\u0540"+ + "\101\0\1\u05ff\63\0\1\u0600\41\0\1\u0601\75\0\1\u0602"+ + "\63\0\1\u0543\4\0\2\u0603\40\0\1\u0604\55\0\1\u0605"+ + "\62\0\1\u0606\57\0\1\u0607\77\0\1\u0608\100\0\1\u0609"+ + "\62\0\1\u060a\25\0\1\u060b\75\0\1\u060c\63\0\1\u060d"+ + "\37\0\1\u060e\71\0\2\u060f\10\0\1\u0610\4\0\2\u0611"+ + "\4\0\1\u0612\53\0\2\u0613\31\0\1\u0614\67\0\1\u0615"+ + "\112\0\1\u0616\22\0\1\u0617\67\0\1\u0618\62\0\1\u0619"+ + "\62\0\2\u061a\57\0\1\u061b\55\0\1\u061c\56\0\1\u061d"+ + "\120\0\1\u061e\24\0\1\u0262\106\0\2\u061f\1\u0620\33\0"+ + "\1\u0621\100\0\1\u0622\57\0\1\u0623\63\0\1\u0624\76\0"+ + "\1\u0625\32\0\2\u0626\53\0\1\u0627\76\0\1\u0628\77\0"+ + "\1\u0629\27\0\1\u062a\55\0\1\u062b\106\0\2\u062c\55\0"+ + "\1\u062d\57\0\1\u062e\100\0\1\u062f\51\0\2\u0630\60\0"+ + "\2\u0631\16\0\2\7\11\0\1\7\1\0\2\7\1\u0632"+ + "\3\7\1\0\1\7\6\0\3\7\1\0\1\7\1\0"+ + "\1\7\1\0\15\7\2\0\2\7\11\0\1\u0633\1\0"+ + "\6\7\1\0\1\7\6\0\3\7\1\0\1\7\1\0"+ + "\1\7\1\0\15\7\2\0\2\7\11\0\1\7\1\0"+ + "\6\7\1\0\1\7\6\0\1\7\1\u0634\1\7\1\0"+ + "\1\7\1\0\1\7\1\0\15\7\2\0\2\7\11\0"+ + "\1\7\1\0\6\7\1\0\1\7\6\0\3\7\1\0"+ + "\1\7\1\0\1\7\1\u05d4\1\u0635\14\7\2\0\2\7"+ + "\11\0\1\7\1\0\6\7\1\0\1\7\6\0\3\7"+ + "\1\0\1\7\1\0\1\7\1\0\1\7\1\u0636\13\7"+ + "\2\0\2\7\11\0\1\u0637\1\0\6\7\1\0\1\7"+ + "\6\0\3\7\1\0\1\7\1\0\1\7\1\0\15\7"+ + "\2\0\2\7\11\0\1\7\1\0\6\7\1\u05d7\1\u0638"+ + "\6\0\3\7\1\0\1\7\1\0\1\7\1\0\15\7"+ + "\2\0\2\7\11\0\1\7\1\0\6\7\1\0\1\7"+ + "\6\0\3\7\1\0\1\7\1\0\1\7\1\0\7\7"+ + "\1\u0639\5\7\2\0\2\7\11\0\1\u063a\1\0\6\7"+ + "\1\0\1\7\6\0\3\7\1\0\1\7\1\0\1\7"+ + "\1\0\15\7\2\0\2\7\11\0\1\7\1\0\6\7"+ + "\1\317\1\320\6\0\3\7\1\0\1\7\1\0\1\7"+ + "\1\0\15\7\2\0\2\7\11\0\1\7\1\0\1\u063b"+ + "\5\7\1\0\1\7\6\0\3\7\1\0\1\7\1\0"+ + "\1\7\1\0\15\7\2\0\2\7\11\0\1\7\1\0"+ + "\6\7\1\0\1\7\6\0\3\7\1\0\1\7\1\0"+ + "\1\7\1\u05db\1\u063c\14\7\2\0\2\7\11\0\1\7"+ + "\1\0\1\u063d\5\7\1\0\1\7\6\0\3\7\1\0"+ + "\1\7\1\0\1\7\1\0\15\7\2\0\2\7\11\0"+ + "\1\7\1\0\6\7\1\0\1\7\6\0\3\7\1\0"+ + "\1\7\1\0\1\7\1\0\1\7\1\u063e\13\7\2\0"+ + "\2\7\11\0\1\7\1\0\4\7\1\u063f\1\7\1\0"+ + "\1\7\6\0\3\7\1\0\1\7\1\0\1\7\1\0"+ + "\15\7\2\0\2\7\11\0\1\7\1\0\2\7\1\u0640"+ + "\3\7\1\0\1\7\6\0\3\7\1\0\1\7\1\0"+ + "\1\7\1\0\15\7\2\0\2\7\11\0\1\7\1\0"+ + "\6\7\1\0\1\7\6\0\1\u0641\2\7\1\0\1\7"+ + "\1\0\1\7\1\0\15\7\2\0\2\7\11\0\1\7"+ + "\1\0\6\7\1\0\1\7\6\0\3\7\1\0\1\7"+ + "\1\0\1\7\1\0\4\7\1\u0642\10\7\2\0\2\7"+ + "\11\0\1\7\1\0\1\u0643\5\7\1\0\1\7\6\0"+ + "\3\7\1\0\1\7\1\0\1\7\1\0\15\7\2\0"+ + "\2\7\11\0\1\7\1\0\6\7\1\0\1\7\6\0"+ + "\3\7\1\0\1\7\1\u05e3\1\u0644\1\0\15\7\2\0"+ + "\2\7\11\0\1\u0645\1\0\6\7\1\0\1\7\6\0"+ + "\3\7\1\0\1\7\1\0\1\7\1\0\15\7\2\0"+ + "\2\7\11\0\1\7\1\0\6\7\1\0\1\7\6\0"+ + "\1\u0646\2\7\1\0\1\7\1\0\1\7\1\0\15\7"+ + "\2\0\2\7\11\0\1\7\1\0\6\7\1\0\1\7"+ + "\6\0\3\7\1\0\1\7\1\0\1\7\1\u05e6\1\u0647"+ + "\14\7\2\0\2\7\11\0\1\7\1\0\1\7\1\u0648"+ + "\4\7\1\0\1\7\6\0\3\7\1\0\1\7\1\0"+ + "\1\7\1\0\15\7\2\0\2\7\11\0\1\7\1\0"+ + "\3\7\1\u0649\2\7\1\0\1\7\6\0\3\7\1\0"+ + "\1\7\1\0\1\7\1\0\15\7\2\0\2\7\11\0"+ + "\1\u064a\1\0\6\7\1\0\1\7\6\0\3\7\1\0"+ + "\1\7\1\0\1\7\1\0\15\7\2\0\2\7\11\0"+ + "\1\7\1\0\6\7\1\0\1\7\6\0\3\7\1\0"+ + "\1\7\1\0\1\7\1\0\1\7\1\u064b\13\7\2\0"+ + "\2\7\11\0\1\7\1\0\6\7\1\0\1\7\6\0"+ + "\3\7\1\0\1\7\1\0\1\7\1\0\10\7\1\273"+ + "\4\7\2\0\2\7\11\0\1\7\1\0\6\7\1\0"+ + "\1\7\6\0\3\7\1\0\1\7\1\0\1\7\1\u05eb"+ + "\1\u064c\14\7\2\0\2\7\11\0\1\7\1\0\6\7"+ + "\1\0\1\7\6\0\2\7\1\u064d\1\0\1\7\1\0"+ + "\1\7\1\0\15\7\2\0\2\7\11\0\1\7\1\0"+ + "\6\7\1\0\1\7\6\0\2\7\1\u064e\1\0\1\7"+ + "\1\0\1\7\1\0\15\7\2\0\2\7\11\0\1\7"+ + "\1\0\6\7\1\0\1\7\6\0\3\7\1\0\1\7"+ + "\1\0\1\7\1\u05ee\1\u064f\14\7\2\0\2\7\11\0"+ + "\1\7\1\0\6\7\1\0\1\7\6\0\3\7\1\0"+ + "\1\7\1\0\1\7\1\0\2\7\1\u0650\12\7\2\0"+ + "\1\u0651\1\7\11\0\1\7\1\0\6\7\1\0\1\7"+ + "\6\0\3\7\1\0\1\7\1\0\1\7\1\0\15\7"+ + "\2\0\2\7\11\0\1\7\1\0\6\7\1\0\1\7"+ + "\6\0\1\u0651\2\7\1\0\1\7\1\0\1\7\1\0"+ + "\15\7\2\0\2\7\11\0\1\7\1\0\1\u0652\5\7"+ + "\1\0\1\7\6\0\3\7\1\0\1\7\1\0\1\7"+ + "\1\0\15\7\2\0\2\7\11\0\1\7\1\0\6\7"+ + "\1\0\1\7\6\0\3\7\1\0\1\7\1\0\1\7"+ + "\1\0\4\7\1\u0653\10\7\2\0\2\7\11\0\1\7"+ + "\1\0\6\7\1\0\1\7\6\0\3\7\1\0\1\7"+ + "\1\u0531\1\u0594\1\u0532\1\u0595\14\7\2\0\2\7\11\0"+ + "\1\u0654\1\0\6\7\1\0\1\7\6\0\3\7\1\0"+ + "\1\7\1\0\1\7\1\0\15\7\2\0\2\7\11\0"+ + "\1\7\1\0\1\u0655\5\7\1\0\1\7\6\0\3\7"+ + "\1\0\1\7\1\0\1\7\1\0\15\7\2\0\2\7"+ + "\11\0\1\7\1\0\6\7\1\0\1\7\6\0\2\7"+ + "\1\u0656\1\0\1\7\1\0\1\7\1\0\15\7\2\0"+ + "\2\7\11\0\1\7\1\0\6\7\1\0\1\7\6\0"+ + "\1\u0657\2\7\1\0\1\7\1\0\1\7\1\0\15\7"+ + "\2\0\2\7\11\0\1\7\1\0\5\7\1\u0658\1\0"+ + "\1\7\6\0\3\7\1\0\1\7\1\0\1\7\1\0"+ + "\15\7\2\0\2\7\11\0\1\7\1\0\1\u0659\5\7"+ + "\1\0\1\7\6\0\3\7\1\0\1\7\1\0\1\7"+ + "\1\0\15\7\2\0\2\7\11\0\1\7\1\0\6\7"+ + "\1\0\1\7\6\0\3\7\1\0\1\7\1\0\1\7"+ + "\1\u05f9\1\u065a\14\7\2\0\2\7\11\0\1\7\1\0"+ + "\6\7\1\0\1\7\6\0\2\7\1\u065b\1\0\1\7"+ + "\1\0\1\7\1\0\15\7\2\0\2\7\11\0\1\7"+ + "\1\0\5\7\1\u065c\1\0\1\7\6\0\3\7\1\0"+ + "\1\7\1\0\1\7\1\0\15\7\2\0\2\7\11\0"+ + "\1\7\1\0\6\7\1\0\1\7\6\0\3\7\1\0"+ + "\1\7\1\0\1\7\1\u05fc\1\u065d\14\7\2\0\2\7"+ + "\11\0\1\7\1\0\6\7\1\0\1\7\6\0\2\7"+ + "\1\u065e\1\0\1\7\1\0\1\7\1\0\15\7\2\0"+ + "\2\7\11\0\1\7\1\0\4\7\1\u065f\1\7\1\0"+ + "\1\7\6\0\3\7\1\0\1\7\1\0\1\7\1\0"+ + "\15\7\2\0\2\7\11\0\1\7\1\0\1\u05a3\5\7"+ + "\1\0\1\7\6\0\3\7\1\0\1\7\1\0\1\7"+ + "\1\0\15\7\2\0\2\7\11\0\1\7\1\0\6\7"+ + "\1\0\1\7\6\0\2\7\1\u0660\1\0\1\7\1\0"+ + "\1\7\1\0\15\7\2\0\2\7\11\0\1\7\1\0"+ + "\6\7\1\0\1\7\6\0\3\7\1\0\1\u0661\1\0"+ + "\1\7\1\0\15\7\2\0\2\7\11\0\1\7\1\0"+ + "\2\7\1\u0662\3\7\1\0\1\7\6\0\3\7\1\0"+ + "\1\7\1\0\1\7\1\0\15\7\2\0\2\7\11\0"+ + "\1\7\1\0\6\7\1\0\1\7\6\0\1\u0663\2\7"+ + "\1\0\1\7\1\0\1\7\1\0\15\7\2\0\2\7"+ + "\11\0\1\7\1\0\6\7\1\0\1\7\6\0\2\7"+ + "\1\u05a6\1\0\1\7\1\0\1\7\1\u0603\1\u0664\14\7"+ + "\2\0\2\7\11\0\1\7\1\0\5\7\1\u0665\1\0"+ + "\1\7\6\0\3\7\1\0\1\7\1\0\1\7\1\0"+ + "\15\7\2\0\2\7\11\0\1\7\1\0\1\7\1\u0666"+ + "\4\7\1\0\1\7\6\0\3\7\1\0\1\7\1\0"+ + "\1\7\1\0\15\7\2\0\2\7\11\0\1\7\1\0"+ + "\2\7\1\u0667\3\7\1\0\1\7\6\0\3\7\1\0"+ + "\1\7\1\0\1\7\1\0\15\7\2\0\2\7\11\0"+ + "\1\7\1\0\1\u0668\5\7\1\0\1\7\6\0\3\7"+ + "\1\0\1\7\1\0\1\7\1\0\15\7\2\0\2\7"+ + "\11\0\1\7\1\0\6\7\1\0\1\7\6\0\1\u0669"+ + "\2\7\1\0\1\7\1\0\1\7\1\0\15\7\2\0"+ + "\2\7\11\0\1\7\1\0\6\7\1\0\1\7\6\0"+ + "\3\7\1\0\1\7\1\0\1\7\1\0\7\7\1\u066a"+ + "\5\7\2\0\2\7\11\0\1\7\1\0\6\7\1\0"+ + "\1\7\6\0\3\7\1\0\1\7\1\0\1\7\1\0"+ + "\10\7\1\u066b\4\7\2\0\2\7\11\0\1\7\1\0"+ + "\2\7\1\u066c\3\7\1\0\1\7\6\0\3\7\1\0"+ + "\1\7\1\0\1\7\1\0\15\7\2\0\2\7\11\0"+ + "\1\7\1\0\6\7\1\0\1\7\6\0\1\u066d\2\7"+ + "\1\0\1\7\1\0\1\7\1\0\15\7\2\0\2\7"+ + "\11\0\1\7\1\0\6\7\1\0\1\7\6\0\2\7"+ + "\1\u066e\1\0\1\7\1\0\1\7\1\0\15\7\2\0"+ + "\2\7\11\0\1\u066f\1\0\6\7\1\0\1\7\6\0"+ + "\3\7\1\0\1\7\1\0\1\7\1\0\15\7\2\0"+ + "\2\7\11\0\1\7\1\0\6\7\1\u060f\1\u0670\6\0"+ + "\2\7\1\u0671\1\0\1\7\1\0\1\7\1\u0611\1\u0672"+ + "\4\7\1\u0673\7\7\2\0\2\7\11\0\1\7\1\0"+ + "\6\7\1\0\1\7\6\0\3\7\1\0\1\7\1\0"+ + "\1\7\1\u0613\1\u0674\14\7\2\0\2\7\11\0\1\u0675"+ + "\1\0\6\7\1\0\1\7\6\0\3\7\1\0\1\7"+ + "\1\0\1\7\1\0\15\7\2\0\2\7\11\0\1\7"+ + "\1\0\4\7\1\u0676\1\7\1\0\1\7\6\0\3\7"+ + "\1\0\1\7\1\0\1\7\1\0\15\7\2\0\2\7"+ + "\11\0\1\7\1\0\6\7\1\0\1\7\6\0\3\7"+ + "\1\0\1\7\1\0\1\7\1\0\7\7\1\u0677\5\7"+ + "\2\0\2\7\11\0\1\u0678\1\0\6\7\1\0\1\7"+ + "\6\0\3\7\1\0\1\7\1\0\1\7\1\0\15\7"+ + "\2\0\2\7\11\0\1\7\1\0\4\7\1\u0679\1\7"+ + "\1\0\1\7\6\0\3\7\1\0\1\7\1\0\1\7"+ + "\1\0\15\7\2\0\2\7\11\0\1\7\1\0\5\7"+ + "\1\u067a\1\0\1\7\6\0\3\7\1\0\1\7\1\0"+ + "\1\7\1\0\15\7\2\0\2\7\11\0\1\7\1\0"+ + "\6\7\1\u061a\1\u067b\6\0\3\7\1\0\1\7\1\0"+ + "\1\7\1\0\15\7\2\0\2\7\11\0\1\7\1\0"+ + "\5\7\1\u067c\1\0\1\7\6\0\3\7\1\0\1\7"+ + "\1\0\1\7\1\0\15\7\2\0\2\7\11\0\1\7"+ + "\1\0\1\7\1\u067d\4\7\1\0\1\7\6\0\3\7"+ + "\1\0\1\7\1\0\1\7\1\0\15\7\2\0\2\7"+ + "\11\0\1\u067e\1\0\6\7\1\0\1\7\6\0\3\7"+ + "\1\0\1\7\1\0\1\7\1\0\15\7\2\0\2\7"+ + "\11\0\1\7\1\0\6\7\1\0\1\7\6\0\3\7"+ + "\1\0\1\7\1\0\1\7\1\0\7\7\1\u067f\5\7"+ + "\2\0\2\7\11\0\1\7\1\0\1\u0263\5\7\1\0"+ + "\1\7\6\0\3\7\1\0\1\7\1\0\1\7\1\0"+ + "\15\7\2\0\2\7\11\0\1\7\1\0\6\7\1\0"+ + "\1\7\6\0\3\7\1\0\1\7\1\0\1\7\1\u061f"+ + "\1\u0680\1\u0681\13\7\2\0\2\7\11\0\1\7\1\0"+ + "\1\7\1\u0682\4\7\1\0\1\7\6\0\3\7\1\0"+ + "\1\7\1\0\1\7\1\0\15\7\2\0\2\7\11\0"+ + "\1\7\1\0\6\7\1\0\1\7\6\0\2\7\1\u0683"+ + "\1\0\1\7\1\0\1\7\1\0\15\7\2\0\2\7"+ + "\11\0\1\7\1\0\6\7\1\0\1\7\6\0\1\u0684"+ + "\2\7\1\0\1\7\1\0\1\7\1\0\15\7\2\0"+ + "\2\7\11\0\1\7\1\0\6\7\1\0\1\7\6\0"+ + "\2\7\1\u0685\1\0\1\7\1\0\1\7\1\0\15\7"+ + "\2\0\2\7\11\0\1\7\1\0\6\7\1\0\1\7"+ + "\6\0\3\7\1\0\1\7\1\0\1\7\1\0\7\7"+ + "\1\u0686\5\7\2\0\2\7\11\0\1\7\1\0\6\7"+ + "\1\u0626\1\u0687\6\0\3\7\1\0\1\7\1\0\1\7"+ + "\1\0\15\7\2\0\2\7\11\0\1\7\1\0\1\7"+ + "\1\u0688\4\7\1\0\1\7\6\0\3\7\1\0\1\7"+ + "\1\0\1\7\1\0\15\7\2\0\2\7\11\0\1\7"+ + "\1\0\6\7\1\0\1\7\6\0\1\u0689\2\7\1\0"+ + "\1\7\1\0\1\7\1\0\15\7\2\0\2\7\11\0"+ + "\1\7\1\0\6\7\1\0\1\7\6\0\3\7\1\0"+ + "\1\7\1\0\1\7\1\0\6\7\1\u068a\6\7\2\0"+ + "\2\7\11\0\1\7\1\0\2\7\1\u068b\3\7\1\0"+ + "\1\7\6\0\3\7\1\0\1\7\1\0\1\7\1\0"+ + "\15\7\2\0\2\7\11\0\1\u068c\1\0\6\7\1\0"+ + "\1\7\6\0\3\7\1\0\1\7\1\0\1\7\1\0"+ + "\15\7\2\0\2\7\11\0\1\7\1\0\6\7\1\0"+ + "\1\7\6\0\3\7\1\0\1\7\1\u062c\1\u068d\1\0"+ + "\15\7\2\0\2\7\11\0\1\7\1\0\6\7\1\0"+ + "\1\7\6\0\2\7\1\u068e\1\0\1\7\1\0\1\7"+ + "\1\0\15\7\2\0\2\7\11\0\1\7\1\0\6\7"+ + "\1\0\1\7\6\0\1\u068f\2\7\1\0\1\7\1\0"+ + "\1\7\1\0\15\7\2\0\2\7\11\0\1\7\1\0"+ + "\6\7\1\0\1\7\6\0\3\7\1\0\1\7\1\0"+ + "\1\7\1\0\7\7\1\u0690\5\7\2\0\2\7\11\0"+ + "\1\7\1\0\6\7\1\0\1\7\6\0\3\7\1\0"+ + "\1\7\1\0\1\7\1\u0630\1\u0691\14\7\2\0\2\7"+ + "\11\0\1\7\1\0\6\7\1\0\1\7\6\0\3\7"+ + "\1\0\1\7\1\0\1\7\1\u0631\1\u0692\14\7\24\0"+ + "\1\u0693\73\0\1\u0694\67\0\2\u0695\33\0\1\u0696\62\0"+ + "\1\u0697\105\0\2\u0698\33\0\1\u0699\77\0\1\u069a\50\0"+ + "\1\u069b\2\u069c\15\0\2\u069d\1\u069e\30\0\1\u069f\110\0"+ + "\2\u06a0\31\0\1\u06a1\112\0\1\u06a2\32\0\1\u06a3\112\0"+ + "\1\u024e\36\0\2\u06a4\54\0\1\u06a5\77\0\1\u0610\37\0"+ + "\1\u02f5\67\0\1\u024e\105\0\1\u06a6\47\0\1\u06a7\41\0"+ + "\1\u06a8\21\0\1\u06a9\47\0\2\u0180\53\0\1\u06aa\103\0"+ + "\2\u06ab\74\0\1\u06ac\24\0\1\u06ad\107\0\1\u06ae\32\0"+ + "\1\u06a7\112\0\1\u06af\47\0\1\u05ee\46\0\1\u06b0\11\0"+ + "\1\u06b1\5\0\2\u06b2\37\0\1\u06b3\113\0\1\u06b4\21\0"+ + "\1\u06b5\122\0\1\u06b6\42\0\1\u06b7\46\0\1\u06b8\74\0"+ + "\1\u06b9\41\0\1\u06ba\114\0\1\u06bb\46\0\1\u01da\41\0"+ + "\1\u06bc\117\0\1\u06bd\61\0\1\u0514\62\0\1\u06be\24\0"+ + "\1\u06bf\65\0\2\u06c0\71\0\1\u06c1\73\0\1\u06c2\35\0"+ + "\1\u06c3\60\0\1\u06c4\73\0\1\u06c5\44\0\1\u06c6\117\0"+ + "\1\u06c7\20\0\1\u06c8\61\0\1\u06c9\103\0\1\u06ca\70\0"+ + "\1\u06cb\34\0\1\u06cc\75\0\1\u06cd\72\0\1\u06ce\54\0"+ + "\1\u06cf\37\0\1\u06d0\101\0\1\u06d1\41\0\1\u06d2\101\0"+ + "\1\u06d3\57\0\1\u06d4\41\0\1\u0514\105\0\1\u06d5\74\0"+ + "\1\u06d6\22\0\1\u06d7\63\0\1\u06d8\57\0\1\u06d9\61\0"+ + "\1\u06a8\21\0\1\u061d\4\0\2\u06da\40\0\1\u06db\103\0"+ + "\1\u06dc\40\0\2\317\27\0\1\u06ac\24\0\1\u06dd\60\0"+ + "\1\u06de\107\0\1\u06df\32\0\1\u06e0\63\0\1\u06e1\55\0"+ + "\1\u06e2\71\0\2\u06e3\52\0\1\u06e4\63\0\1\u06e5\106\0"+ + "\1\u06e6\1\u040d\37\0\2\u06e7\103\0\1\u06e8\26\0\1\u06e9"+ + "\65\0\1\u06ea\61\0\1\u024e\77\0\1\u06eb\71\0\1\u06ec"+ + "\47\0\1\u06ed\26\0\2\7\11\0\1\7\1\0\5\7"+ + "\1\u06ee\1\0\1\7\6\0\3\7\1\0\1\7\1\0"+ + "\1\7\1\0\15\7\2\0\2\7\11\0\1\7\1\0"+ + "\6\7\1\0\1\7\6\0\1\7\1\u06ef\1\7\1\0"+ + "\1\7\1\0\1\7\1\0\15\7\2\0\2\7\11\0"+ + "\1\7\1\0\6\7\1\0\1\7\6\0\3\7\1\0"+ + "\1\7\1\0\1\7\1\u0695\1\u06f0\14\7\2\0\2\7"+ + "\11\0\1\7\1\0\1\u06f1\5\7\1\0\1\7\6\0"+ + "\3\7\1\0\1\7\1\0\1\7\1\0\15\7\2\0"+ + "\2\7\11\0\1\7\1\0\1\7\1\u06f2\4\7\1\0"+ + "\1\7\6\0\3\7\1\0\1\7\1\0\1\7\1\0"+ + "\15\7\2\0\2\7\11\0\1\7\1\0\6\7\1\0"+ + "\1\7\6\0\3\7\1\0\1\7\1\0\1\7\1\u0698"+ + "\1\u06f3\14\7\2\0\2\7\11\0\1\7\1\0\1\u06f4"+ + "\5\7\1\0\1\7\6\0\3\7\1\0\1\7\1\0"+ + "\1\7\1\0\15\7\2\0\2\7\11\0\1\7\1\0"+ + "\6\7\1\0\1\7\6\0\1\321\2\7\1\0\1\7"+ + "\1\0\1\7\1\0\15\7\2\0\2\7\11\0\1\7"+ + "\1\0\5\7\1\u06f5\1\u069c\1\u06f6\6\0\3\7\1\0"+ + "\1\7\1\0\1\7\1\u069d\1\u06f7\1\u06f8\13\7\2\0"+ + "\2\7\11\0\1\u06f9\1\0\6\7\1\0\1\7\6\0"+ + "\3\7\1\0\1\7\1\0\1\7\1\0\15\7\2\0"+ + "\2\7\11\0\1\7\1\0\6\7\1\0\1\7\6\0"+ + "\3\7\1\0\1\7\1\0\1\7\1\u06a0\1\u06fa\14\7"+ + "\2\0\2\7\11\0\1\u06fb\1\0\6\7\1\0\1\7"+ + "\6\0\3\7\1\0\1\7\1\0\1\7\1\0\15\7"+ + "\2\0\2\7\11\0\1\7\1\0\6\7\1\0\1\7"+ + "\6\0\3\7\1\0\1\7\1\0\1\7\1\0\1\7"+ + "\1\u06fc\13\7\2\0\2\7\11\0\1\7\1\0\1\u06fd"+ + "\5\7\1\0\1\7\6\0\3\7\1\0\1\7\1\0"+ + "\1\7\1\0\15\7\2\0\2\7\11\0\1\7\1\0"+ + "\6\7\1\0\1\7\6\0\3\7\1\0\1\7\1\0"+ + "\1\7\1\0\3\7\1\273\11\7\2\0\2\7\11\0"+ + "\1\7\1\0\6\7\1\u06a4\1\u06fe\6\0\3\7\1\0"+ + "\1\7\1\0\1\7\1\0\15\7\2\0\2\7\11\0"+ + "\1\7\1\0\2\7\1\u06ff\3\7\1\0\1\7\6\0"+ + "\3\7\1\0\1\7\1\0\1\7\1\0\15\7\2\0"+ + "\2\7\11\0\1\7\1\0\6\7\1\0\1\7\6\0"+ + "\2\7\1\u0671\1\0\1\7\1\0\1\7\1\0\15\7"+ + "\2\0\2\7\11\0\1\u02f6\1\0\6\7\1\0\1\7"+ + "\6\0\3\7\1\0\1\7\1\0\1\7\1\0\15\7"+ + "\2\0\2\7\11\0\1\7\1\0\4\7\1\273\1\7"+ + "\1\0\1\7\6\0\3\7\1\0\1\7\1\0\1\7"+ + "\1\0\15\7\2\0\2\7\11\0\1\7\1\0\6\7"+ + "\1\0\1\7\6\0\3\7\1\0\1\7\1\0\1\7"+ + "\1\0\2\7\1\u0700\12\7\2\0\2\7\11\0\1\7"+ + "\1\0\6\7\1\0\1\7\6\0\1\u0701\2\7\1\0"+ + "\1\7\1\0\1\7\1\0\15\7\2\0\2\7\11\0"+ + "\1\u0702\1\0\6\7\1\0\1\7\6\0\2\7\1\u0703"+ + "\1\0\1\7\1\0\1\7\1\0\15\7\2\0\2\7"+ + "\11\0\1\7\1\0\6\7\1\u0180\1\u0181\6\0\3\7"+ + "\1\0\1\7\1\0\1\7\1\0\15\7\2\0\2\7"+ + "\11\0\1\7\1\0\1\7\1\u0704\4\7\1\0\1\7"+ + "\6\0\3\7\1\0\1\7\1\0\1\7\1\0\15\7"+ + "\2\0\2\7\11\0\1\7\1\0\6\7\1\0\1\7"+ + "\6\0\3\7\1\0\1\7\1\u06ab\1\u0705\1\0\15\7"+ + "\2\0\2\7\11\0\1\7\1\0\6\7\1\0\1\7"+ + "\6\0\3\7\1\0\1\7\1\0\1\7\1\0\11\7"+ + "\1\u0706\3\7\2\0\2\7\11\0\1\7\1\0\2\7"+ + "\1\u0707\3\7\1\0\1\7\6\0\3\7\1\0\1\7"+ + "\1\0\1\7\1\0\15\7\2\0\2\7\11\0\1\7"+ + "\1\0\6\7\1\0\1\7\6\0\3\7\1\0\1\7"+ + "\1\0\1\7\1\0\2\7\1\u0708\12\7\2\0\2\7"+ + "\11\0\1\7\1\0\1\7\1\u0701\4\7\1\0\1\7"+ + "\6\0\3\7\1\0\1\7\1\0\1\7\1\0\15\7"+ + "\2\0\2\7\11\0\1\7\1\0\6\7\1\0\1\7"+ + "\6\0\3\7\1\0\1\7\1\0\1\7\1\0\4\7"+ + "\1\u0709\10\7\2\0\2\7\11\0\1\7\1\0\6\7"+ + "\1\0\1\7\6\0\2\7\1\u064f\1\0\1\7\1\0"+ + "\1\7\1\0\15\7\2\0\2\7\11\0\1\7\1\0"+ + "\5\7\1\u070a\1\0\1\7\6\0\1\7\1\u070b\1\7"+ + "\1\0\1\7\1\0\1\7\1\u06b2\1\u070c\14\7\2\0"+ + "\2\7\11\0\1\7\1\0\4\7\1\u070d\1\7\1\0"+ + "\1\7\6\0\3\7\1\0\1\7\1\0\1\7\1\0"+ + "\15\7\2\0\2\7\11\0\1\7\1\0\6\7\1\0"+ + "\1\7\6\0\3\7\1\0\1\7\1\0\1\7\1\0"+ + "\10\7\1\u070e\4\7\2\0\2\7\11\0\1\u070f\1\0"+ + "\6\7\1\0\1\7\6\0\3\7\1\0\1\7\1\0"+ + "\1\7\1\0\15\7\2\0\2\7\11\0\1\7\1\0"+ + "\6\7\1\0\1\7\6\0\3\7\1\0\1\7\1\0"+ + "\1\7\1\0\11\7\1\u0710\3\7\2\0\2\7\11\0"+ + "\1\7\1\0\6\7\1\0\1\7\6\0\2\7\1\u0711"+ + "\1\0\1\7\1\0\1\7\1\0\15\7\2\0\2\7"+ + "\11\0\1\7\1\0\5\7\1\u0712\1\0\1\7\6\0"+ + "\3\7\1\0\1\7\1\0\1\7\1\0\15\7\2\0"+ + "\2\7\11\0\1\7\1\0\6\7\1\0\1\7\6\0"+ + "\2\7\1\u0713\1\0\1\7\1\0\1\7\1\0\15\7"+ + "\2\0\2\7\11\0\1\7\1\0\1\u0714\5\7\1\0"+ + "\1\7\6\0\3\7\1\0\1\7\1\0\1\7\1\0"+ + "\15\7\2\0\2\7\11\0\1\7\1\0\6\7\1\0"+ + "\1\7\6\0\3\7\1\0\1\7\1\0\1\7\1\0"+ + "\5\7\1\u0715\7\7\2\0\2\7\11\0\1\7\1\0"+ + "\6\7\1\0\1\7\6\0\2\7\1\u011c\1\0\1\7"+ + "\1\0\1\7\1\0\15\7\2\0\2\7\11\0\1\7"+ + "\1\0\1\u0716\5\7\1\0\1\7\6\0\3\7\1\0"+ + "\1\7\1\0\1\7\1\0\15\7\2\0\2\7\11\0"+ + "\1\7\1\0\6\7\1\0\1\7\6\0\3\7\1\0"+ + "\1\7\1\0\1\7\1\0\10\7\1\u0717\4\7\2\0"+ + "\2\7\11\0\1\7\1\0\6\7\1\0\1\7\6\0"+ + "\3\7\1\0\1\7\1\0\1\7\1\0\10\7\1\u0577"+ + "\4\7\2\0\2\7\11\0\1\7\1\0\6\7\1\0"+ + "\1\7\6\0\3\7\1\0\1\7\1\0\1\7\1\0"+ + "\11\7\1\u0718\3\7\2\0\2\7\11\0\1\7\1\0"+ + "\2\7\1\u0719\3\7\1\0\1\7\6\0\3\7\1\0"+ + "\1\7\1\0\1\7\1\0\15\7\2\0\2\7\11\0"+ + "\1\7\1\0\6\7\1\u06c0\1\u071a\6\0\3\7\1\0"+ + "\1\7\1\0\1\7\1\0\15\7\2\0\2\7\11\0"+ + "\1\7\1\0\6\7\1\0\1\7\6\0\1\7\1\u071b"+ + "\1\7\1\0\1\7\1\0\1\7\1\0\15\7\2\0"+ + "\2\7\11\0\1\7\1\0\6\7\1\0\1\7\6\0"+ + "\3\7\1\0\1\7\1\0\1\7\1\0\3\7\1\u071c"+ + "\11\7\2\0\2\7\11\0\1\7\1\0\5\7\1\u071d"+ + "\1\0\1\7\6\0\3\7\1\0\1\7\1\0\1\7"+ + "\1\0\15\7\2\0\2\7\11\0\1\7\1\0\4\7"+ + "\1\u071e\1\7\1\0\1\7\6\0\3\7\1\0\1\7"+ + "\1\0\1\7\1\0\15\7\2\0\2\7\11\0\1\7"+ + "\1\0\6\7\1\0\1\7\6\0\1\u071f\2\7\1\0"+ + "\1\7\1\0\1\7\1\0\15\7\2\0\2\7\11\0"+ + "\1\7\1\0\1\7\1\u0720\4\7\1\0\1\7\6\0"+ + "\3\7\1\0\1\7\1\0\1\7\1\0\15\7\2\0"+ + "\2\7\11\0\1\7\1\0\6\7\1\0\1\7\6\0"+ + "\3\7\1\0\1\7\1\0\1\7\1\0\11\7\1\u0721"+ + "\3\7\2\0\2\7\11\0\1\u0722\1\0\6\7\1\0"+ + "\1\7\6\0\3\7\1\0\1\7\1\0\1\7\1\0"+ + "\15\7\2\0\2\7\11\0\1\u0723\1\0\6\7\1\0"+ + "\1\7\6\0\3\7\1\0\1\7\1\0\1\7\1\0"+ + "\15\7\2\0\2\7\11\0\1\7\1\0\6\7\1\0"+ + "\1\7\6\0\2\7\1\u0724\1\0\1\7\1\0\1\7"+ + "\1\0\15\7\2\0\2\7\11\0\1\7\1\0\6\7"+ + "\1\0\1\7\6\0\3\7\1\0\1\7\1\0\1\7"+ + "\1\0\1\7\1\u0725\13\7\2\0\2\7\11\0\1\7"+ + "\1\0\2\7\1\u0726\3\7\1\0\1\7\6\0\3\7"+ + "\1\0\1\7\1\0\1\7\1\0\15\7\2\0\2\7"+ + "\11\0\1\7\1\0\6\7\1\0\1\7\6\0\1\u0727"+ + "\2\7\1\0\1\7\1\0\1\7\1\0\15\7\2\0"+ + "\2\7\11\0\1\7\1\0\6\7\1\0\1\7\6\0"+ + "\3\7\1\0\1\7\1\0\1\7\1\0\1\7\1\u0728"+ + "\13\7\2\0\2\7\11\0\1\7\1\0\6\7\1\0"+ + "\1\7\6\0\3\7\1\0\1\u0729\1\0\1\7\1\0"+ + "\15\7\2\0\2\7\11\0\1\7\1\0\1\u072a\5\7"+ + "\1\0\1\7\6\0\3\7\1\0\1\7\1\0\1\7"+ + "\1\0\15\7\2\0\2\7\11\0\1\7\1\0\6\7"+ + "\1\0\1\7\6\0\2\7\1\u072b\1\0\1\7\1\0"+ + "\1\7\1\0\15\7\2\0\2\7\11\0\1\7\1\0"+ + "\1\u072c\5\7\1\0\1\7\6\0\3\7\1\0\1\7"+ + "\1\0\1\7\1\0\15\7\2\0\2\7\11\0\1\7"+ + "\1\0\6\7\1\0\1\7\6\0\2\7\1\u072d\1\0"+ + "\1\7\1\0\1\7\1\0\15\7\2\0\2\7\11\0"+ + "\1\7\1\0\6\7\1\0\1\7\6\0\1\u072e\2\7"+ + "\1\0\1\7\1\0\1\7\1\0\15\7\2\0\2\7"+ + "\11\0\1\u0577\1\0\6\7\1\0\1\7\6\0\3\7"+ + "\1\0\1\7\1\0\1\7\1\0\15\7\2\0\2\7"+ + "\11\0\1\7\1\0\6\7\1\0\1\7\6\0\3\7"+ + "\1\0\1\u072f\1\0\1\7\1\0\15\7\2\0\2\7"+ + "\11\0\1\7\1\0\6\7\1\0\1\7\6\0\3\7"+ + "\1\0\1\7\1\0\1\7\1\0\7\7\1\u0730\5\7"+ + "\2\0\2\7\11\0\1\u0731\1\0\6\7\1\0\1\7"+ + "\6\0\3\7\1\0\1\7\1\0\1\7\1\0\15\7"+ + "\2\0\2\7\11\0\1\7\1\0\1\u0732\5\7\1\0"+ + "\1\7\6\0\3\7\1\0\1\7\1\0\1\7\1\0"+ + "\15\7\2\0\2\7\11\0\1\u0733\1\0\6\7\1\0"+ + "\1\7\6\0\3\7\1\0\1\7\1\0\1\7\1\0"+ + "\15\7\2\0\2\7\11\0\1\u0702\1\0\6\7\1\0"+ + "\1\7\6\0\2\7\1\u067e\1\0\1\7\1\0\1\7"+ + "\1\u06da\1\u0734\14\7\2\0\2\7\11\0\1\7\1\0"+ + "\5\7\1\u0735\1\0\1\7\6\0\3\7\1\0\1\7"+ + "\1\0\1\7\1\0\15\7\2\0\2\7\11\0\1\7"+ + "\1\0\6\7\1\0\1\7\6\0\3\7\1\0\1\7"+ + "\1\0\1\7\1\0\1\7\1\u0736\13\7\2\0\2\7"+ + "\11\0\1\7\1\0\6\7\1\317\1\320\6\0\3\7"+ + "\1\0\1\7\1\0\1\7\1\0\11\7\1\u0706\3\7"+ + "\2\0\2\7\11\0\1\7\1\0\2\7\1\u0737\3\7"+ + "\1\0\1\7\6\0\3\7\1\0\1\7\1\0\1\7"+ + "\1\0\15\7\2\0\2\7\11\0\1\7\1\0\1\7"+ + "\1\u0738\4\7\1\0\1\7\6\0\3\7\1\0\1\7"+ + "\1\0\1\7\1\0\15\7\2\0\2\7\11\0\1\7"+ + "\1\0\6\7\1\0\1\7\6\0\3\7\1\0\1\7"+ + "\1\0\1\7\1\0\1\7\1\u0739\13\7\2\0\2\7"+ + "\11\0\1\7\1\0\1\u073a\5\7\1\0\1\7\6\0"+ + "\3\7\1\0\1\7\1\0\1\7\1\0\15\7\2\0"+ + "\2\7\11\0\1\7\1\0\2\7\1\u073b\3\7\1\0"+ + "\1\7\6\0\3\7\1\0\1\7\1\0\1\7\1\0"+ + "\15\7\2\0\2\7\11\0\1\u073c\1\0\6\7\1\0"+ + "\1\7\6\0\3\7\1\0\1\7\1\0\1\7\1\0"+ + "\15\7\2\0\2\7\11\0\1\7\1\0\6\7\1\u06e3"+ + "\1\u073d\6\0\3\7\1\0\1\7\1\0\1\7\1\0"+ + "\15\7\2\0\2\7\11\0\1\7\1\0\1\u073e\5\7"+ + "\1\0\1\7\6\0\3\7\1\0\1\7\1\0\1\7"+ + "\1\0\15\7\2\0\2\7\11\0\1\7\1\0\2\7"+ + "\1\u073f\3\7\1\0\1\7\6\0\3\7\1\0\1\7"+ + "\1\0\1\7\1\0\15\7\2\0\2\7\11\0\1\7"+ + "\1\0\6\7\1\0\1\7\6\0\3\7\1\0\1\7"+ + "\1\0\1\7\1\0\1\7\1\u0740\1\u0447\12\7\2\0"+ + "\2\7\11\0\1\7\1\0\6\7\1\u06e7\1\u0741\6\0"+ + "\3\7\1\0\1\7\1\0\1\7\1\0\15\7\2\0"+ + "\2\7\11\0\1\7\1\0\6\7\1\0\1\7\6\0"+ + "\3\7\1\0\1\7\1\0\1\7\1\0\3\7\1\u0742"+ + "\11\7\2\0\2\7\11\0\1\u0743\1\0\6\7\1\0"+ + "\1\7\6\0\3\7\1\0\1\7\1\0\1\7\1\0"+ + "\15\7\2\0\2\7\11\0\1\7\1\0\2\7\1\u0744"+ + "\3\7\1\0\1\7\6\0\3\7\1\0\1\7\1\0"+ + "\1\7\1\0\15\7\2\0\2\7\11\0\1\7\1\0"+ + "\2\7\1\273\3\7\1\0\1\7\6\0\3\7\1\0"+ + "\1\7\1\0\1\7\1\0\15\7\2\0\2\7\11\0"+ + "\1\7\1\0\6\7\1\0\1\7\6\0\2\7\1\u0745"+ + "\1\0\1\7\1\0\1\7\1\0\15\7\2\0\2\7"+ + "\11\0\1\7\1\0\6\7\1\0\1\7\6\0\3\7"+ + "\1\0\1\7\1\0\1\7\1\0\2\7\1\u0746\12\7"+ + "\2\0\2\7\11\0\1\7\1\0\6\7\1\0\1\7"+ + "\6\0\1\u0747\2\7\1\0\1\7\1\0\1\7\1\0"+ + "\15\7\17\0\1\u0748\106\0\2\u01da\33\0\1\u0749\106\0"+ + "\2\u074a\31\0\1\u074b\101\0\1\u074c\100\0\1\u074d\26\0"+ + "\1\u074e\61\0\1\u074f\57\0\1\u0750\77\0\1\u0751\50\0"+ + "\1\u0752\24\0\1\u0753\63\0\1\u0754\25\0\1\u0755\116\0"+ + "\1\u0756\44\0\1\u0757\37\0\1\u0758\103\0\1\u0759\42\0"+ + "\1\u075a\62\0\1\u075b\106\0\1\u075c\30\0\1\u075d\70\0"+ + "\1\u075e\61\0\1\u075f\104\0\1\u0760\35\0\1\u0761\74\0"+ + "\1\u0609\47\0\1\u0762\61\0\1\u0763\101\0\2\u0764\60\0"+ + "\2\u0765\33\0\1\u0766\61\0\1\u0767\103\0\1\u0768\74\0"+ + "\1\u0769\51\0\2\u0237\35\0\1\u076a\104\0\2\u076b\33\0"+ + "\1\u076c\57\0\1\u076d\101\0\1\u076e\75\0\1\u076f\33\0"+ + "\1\u0770\53\0\1\u0540\63\0\1\u0771\57\0\1\u0772\110\0"+ + "\2\u0773\33\0\1\u06c1\106\0\2\u0774\62\0\1\u0775\51\0"+ + "\1\u0776\46\0\1\u0777\53\0\1\u0778\101\0\1\u0779\44\0"+ + "\1\u077a\76\0\1\u077b\41\0\1\u06bd\102\0\1\u077c\72\0"+ + "\1\u077d\32\0\1\u077e\43\0\1\u077f\74\0\1\u0780\64\0"+ + "\1\u0781\56\0\1\u0782\114\0\1\u055b\36\0\2\u0783\54\0"+ + "\1\u0784\55\0\1\u0785\71\0\2\u0786\104\0\1\u0787\56\0"+ + "\1\u0788\63\0\1\u0789\35\0\1\u078a\106\0\1\u078b\27\0"+ + "\1\u078c\57\0\1\u078d\106\0\2\u06e2\33\0\1\u078e\5\0"+ + "\1\u078f\73\0\1\u0790\12\0\1\u0791\30\0\1\u0792\114\0"+ + "\1\u0793\27\0\1\u0794\64\0\1\u0478\20\0\2\u06a0\65\0"+ + "\1\u0795\25\0\1\u0796\70\0\1\u0797\112\0\1\u0798\41\0"+ + "\1\u0799\45\0\1\u079a\111\0\1\u079b\54\0\2\u079c\16\0"+ + "\2\7\11\0\1\7\1\0\1\u079d\5\7\1\0\1\7"+ + "\6\0\3\7\1\0\1\7\1\0\1\7\1\0\15\7"+ + "\2\0\2\7\11\0\1\7\1\0\6\7\1\0\1\7"+ + "\6\0\3\7\1\0\1\7\1\0\1\7\1\u01da\1\u011c"+ + "\14\7\2\0\2\7\11\0\1\7\1\0\1\u079e\5\7"+ + "\1\0\1\7\6\0\3\7\1\0\1\7\1\0\1\7"+ + "\1\0\15\7\2\0\2\7\11\0\1\7\1\0\6\7"+ + "\1\0\1\7\6\0\3\7\1\0\1\7\1\0\1\7"+ + "\1\u074a\1\u079f\14\7\2\0\2\7\11\0\1\u07a0\1\0"+ + "\6\7\1\0\1\7\6\0\3\7\1\0\1\7\1\0"+ + "\1\7\1\0\15\7\2\0\2\7\11\0\1\7\1\0"+ + "\6\7\1\0\1\7\6\0\1\u07a1\2\7\1\0\1\7"+ + "\1\0\1\7\1\0\15\7\2\0\2\7\11\0\1\7"+ + "\1\0\6\7\1\0\1\7\6\0\3\7\1\0\1\7"+ + "\1\0\1\7\1\0\7\7\1\u07a2\5\7\2\0\2\7"+ + "\11\0\1\7\1\0\2\7\1\u07a3\3\7\1\0\1\7"+ + "\6\0\3\7\1\0\1\7\1\0\1\7\1\0\15\7"+ + "\2\0\2\7\11\0\1\7\1\0\1\u07a4\5\7\1\0"+ + "\1\7\6\0\3\7\1\0\1\7\1\0\1\7\1\0"+ + "\15\7\2\0\2\7\11\0\1\7\1\0\6\7\1\0"+ + "\1\7\6\0\1\u07a5\2\7\1\0\1\7\1\0\1\7"+ + "\1\0\15\7\2\0\2\7\11\0\1\7\1\0\5\7"+ + "\1\u07a6\1\0\1\7\6\0\3\7\1\0\1\7\1\0"+ + "\1\7\1\0\4\7\1\u07a7\10\7\2\0\2\7\11\0"+ + "\1\7\1\0\6\7\1\0\1\7\6\0\3\7\1\0"+ + "\1\7\1\0\1\7\1\0\6\7\1\u07a8\6\7\2\0"+ + "\2\7\11\0\1\7\1\0\1\u07a9\5\7\1\0\1\7"+ + "\6\0\3\7\1\0\1\7\1\0\1\7\1\0\15\7"+ + "\2\0\2\7\11\0\1\7\1\0\6\7\1\0\1\7"+ + "\6\0\3\7\1\0\1\7\1\0\1\7\1\0\7\7"+ + "\1\u07aa\5\7\2\0\2\7\11\0\1\7\1\0\6\7"+ + "\1\0\1\7\6\0\2\7\1\u07ab\1\0\1\7\1\0"+ + "\1\7\1\0\15\7\2\0\2\7\11\0\1\u07ac\1\0"+ + "\6\7\1\0\1\7\6\0\3\7\1\0\1\7\1\0"+ + "\1\7\1\0\15\7\2\0\2\7\11\0\1\7\1\0"+ + "\6\7\1\0\1\7\6\0\2\7\1\u07ad\1\0\1\7"+ + "\1\0\1\7\1\0\15\7\2\0\2\7\11\0\1\7"+ + "\1\0\1\7\1\u01e5\4\7\1\0\1\7\6\0\3\7"+ + "\1\0\1\7\1\0\1\7\1\0\15\7\2\0\2\7"+ + "\11\0\1\7\1\0\2\7\1\u03de\3\7\1\0\1\7"+ + "\6\0\3\7\1\0\1\7\1\0\1\7\1\0\15\7"+ + "\2\0\2\7\11\0\1\7\1\0\6\7\1\0\1\7"+ + "\6\0\3\7\1\0\1\7\1\0\1\7\1\0\1\7"+ + "\1\u07ae\13\7\2\0\2\7\11\0\1\u07af\1\0\6\7"+ + "\1\0\1\7\6\0\3\7\1\0\1\7\1\0\1\7"+ + "\1\0\15\7\2\0\2\7\11\0\1\7\1\0\5\7"+ + "\1\u07b0\1\0\1\7\6\0\3\7\1\0\1\7\1\0"+ + "\1\7\1\0\15\7\2\0\2\7\11\0\1\7\1\0"+ + "\5\7\1\u07b1\1\0\1\7\6\0\3\7\1\0\1\7"+ + "\1\0\1\7\1\0\15\7\2\0\2\7\11\0\1\7"+ + "\1\0\6\7\1\0\1\7\6\0\3\7\1\0\1\7"+ + "\1\0\1\7\1\0\2\7\1\u07b2\12\7\2\0\2\7"+ + "\11\0\1\7\1\0\4\7\1\u07b3\1\7\1\0\1\7"+ + "\6\0\3\7\1\0\1\7\1\0\1\7\1\0\15\7"+ + "\2\0\2\7\11\0\1\7\1\0\6\7\1\0\1\7"+ + "\6\0\1\7\1\u066a\1\7\1\0\1\7\1\0\1\7"+ + "\1\0\15\7\2\0\2\7\11\0\1\7\1\0\5\7"+ + "\1\u07b4\1\0\1\7\6\0\3\7\1\0\1\7\1\0"+ + "\1\7\1\0\15\7\2\0\2\7\11\0\1\7\1\0"+ + "\5\7\1\u07b5\1\0\1\7\6\0\3\7\1\0\1\7"+ + "\1\0\1\7\1\0\15\7\2\0\2\7\11\0\1\7"+ + "\1\0\6\7\1\0\1\7\6\0\3\7\1\0\1\7"+ + "\1\0\1\7\1\u0764\1\u07b6\14\7\2\0\2\7\11\0"+ + "\1\7\1\0\6\7\1\0\1\7\6\0\3\7\1\0"+ + "\1\7\1\0\1\7\1\u0765\1\u07b7\14\7\2\0\2\7"+ + "\11\0\1\7\1\0\1\u07b8\5\7\1\0\1\7\6\0"+ + "\3\7\1\0\1\7\1\0\1\7\1\0\15\7\2\0"+ + "\2\7\11\0\1\7\1\0\1\u07b9\5\7\1\0\1\7"+ + "\6\0\3\7\1\0\1\7\1\0\1\7\1\0\15\7"+ + "\2\0\2\7\11\0\1\7\1\0\6\7\1\0\1\7"+ + "\6\0\3\7\1\0\1\u07ba\1\0\1\7\1\0\15\7"+ + "\2\0\2\7\11\0\1\7\1\0\6\7\1\0\1\7"+ + "\6\0\3\7\1\0\1\7\1\0\1\7\1\0\7\7"+ + "\1\u07bb\5\7\2\0\2\7\11\0\1\7\1\0\6\7"+ + "\1\0\1\7\6\0\3\7\1\0\1\7\1\0\1\7"+ + "\1\u0237\1\326\14\7\2\0\2\7\11\0\1\7\1\0"+ + "\2\7\1\u07bc\3\7\1\0\1\7\6\0\3\7\1\0"+ + "\1\7\1\0\1\7\1\0\15\7\2\0\2\7\11\0"+ + "\1\7\1\0\6\7\1\0\1\7\6\0\3\7\1\0"+ + "\1\7\1\0\1\7\1\u076b\1\u07bd\14\7\2\0\2\7"+ + "\11\0\1\7\1\0\1\u07be\5\7\1\0\1\7\6\0"+ + "\3\7\1\0\1\7\1\0\1\7\1\0\15\7\2\0"+ + "\2\7\11\0\1\u07bf\1\0\6\7\1\0\1\7\6\0"+ + "\3\7\1\0\1\7\1\0\1\7\1\0\15\7\2\0"+ + "\2\7\11\0\1\7\1\0\6\7\1\0\1\7\6\0"+ + "\1\u07c0\2\7\1\0\1\7\1\0\1\7\1\0\15\7"+ + "\2\0\2\7\11\0\1\7\1\0\6\7\1\0\1\7"+ + "\6\0\3\7\1\0\1\7\1\0\1\7\1\0\4\7"+ + "\1\u07c1\10\7\2\0\2\7\11\0\1\7\1\0\4\7"+ + "\1\u07c2\1\7\1\0\1\7\6\0\3\7\1\0\1\7"+ + "\1\0\1\7\1\0\15\7\2\0\2\7\11\0\1\u05a3"+ + "\1\0\6\7\1\0\1\7\6\0\3\7\1\0\1\7"+ + "\1\0\1\7\1\0\15\7\2\0\2\7\11\0\1\7"+ + "\1\0\1\u07c3\5\7\1\0\1\7\6\0\3\7\1\0"+ + "\1\7\1\0\1\7\1\0\15\7\2\0\2\7\11\0"+ + "\1\u07c4\1\0\6\7\1\0\1\7\6\0\3\7\1\0"+ + "\1\7\1\0\1\7\1\0\15\7\2\0\2\7\11\0"+ + "\1\7\1\0\6\7\1\0\1\7\6\0\3\7\1\0"+ + "\1\7\1\0\1\7\1\u0773\1\u07c5\14\7\2\0\2\7"+ + "\11\0\1\7\1\0\1\u071b\5\7\1\0\1\7\6\0"+ + "\3\7\1\0\1\7\1\0\1\7\1\0\15\7\2\0"+ + "\2\7\11\0\1\7\1\0\6\7\1\0\1\7\6\0"+ + "\3\7\1\0\1\7\1\0\1\7\1\u0774\1\u07c6\14\7"+ + "\2\0\2\7\11\0\1\7\1\0\6\7\1\0\1\7"+ + "\6\0\3\7\1\0\1\7\1\0\1\7\1\0\1\7"+ + "\1\u07c7\13\7\2\0\2\7\11\0\1\7\1\0\6\7"+ + "\1\0\1\7\6\0\1\7\1\u07c8\1\7\1\0\1\7"+ + "\1\0\1\7\1\0\15\7\2\0\2\7\11\0\1\7"+ + "\1\0\4\7\1\u07c9\1\7\1\0\1\7\6\0\3\7"+ + "\1\0\1\7\1\0\1\7\1\0\15\7\2\0\2\7"+ + "\11\0\1\u07ca\1\0\6\7\1\0\1\7\6\0\3\7"+ + "\1\0\1\7\1\0\1\7\1\0\15\7\2\0\2\7"+ + "\11\0\1\7\1\0\6\7\1\0\1\7\6\0\1\u07cb"+ + "\2\7\1\0\1\7\1\0\1\7\1\0\15\7\2\0"+ + "\2\7\11\0\1\7\1\0\1\7\1\u07cc\4\7\1\0"+ + "\1\7\6\0\3\7\1\0\1\7\1\0\1\7\1\0"+ + "\15\7\2\0\2\7\11\0\1\7\1\0\6\7\1\0"+ + "\1\7\6\0\1\u07cd\2\7\1\0\1\7\1\0\1\7"+ + "\1\0\15\7\2\0\2\7\11\0\1\u0717\1\0\6\7"+ + "\1\0\1\7\6\0\3\7\1\0\1\7\1\0\1\7"+ + "\1\0\15\7\2\0\2\7\11\0\1\7\1\0\6\7"+ + "\1\0\1\7\6\0\1\7\1\u07ce\1\7\1\0\1\7"+ + "\1\0\1\7\1\0\15\7\2\0\2\7\11\0\1\7"+ + "\1\0\6\7\1\0\1\7\6\0\3\7\1\0\1\7"+ + "\1\0\1\7\1\0\2\7\1\u07cf\12\7\2\0\2\7"+ + "\11\0\1\7\1\0\1\7\1\u07d0\4\7\1\0\1\7"+ + "\6\0\3\7\1\0\1\7\1\0\1\7\1\0\15\7"+ + "\2\0\1\u07d1\1\7\11\0\1\7\1\0\6\7\1\0"+ + "\1\7\6\0\3\7\1\0\1\7\1\0\1\7\1\0"+ + "\15\7\2\0\2\7\11\0\1\u07d2\1\0\6\7\1\0"+ + "\1\7\6\0\3\7\1\0\1\7\1\0\1\7\1\0"+ + "\15\7\2\0\2\7\11\0\1\7\1\0\1\7\1\u07d3"+ + "\4\7\1\0\1\7\6\0\3\7\1\0\1\7\1\0"+ + "\1\7\1\0\15\7\2\0\2\7\11\0\1\u07d4\1\0"+ + "\6\7\1\0\1\7\6\0\3\7\1\0\1\7\1\0"+ + "\1\7\1\0\15\7\2\0\2\7\11\0\1\7\1\0"+ + "\6\7\1\0\1\7\6\0\3\7\1\0\1\7\1\0"+ + "\1\7\1\0\3\7\1\u05be\11\7\2\0\2\7\11\0"+ + "\1\7\1\0\6\7\1\u0783\1\u07d5\6\0\3\7\1\0"+ + "\1\7\1\0\1\7\1\0\15\7\2\0\2\7\11\0"+ + "\1\7\1\0\2\7\1\u07d6\3\7\1\0\1\7\6\0"+ + "\3\7\1\0\1\7\1\0\1\7\1\0\15\7\2\0"+ + "\2\7\11\0\1\u07d7\1\0\6\7\1\0\1\7\6\0"+ + "\3\7\1\0\1\7\1\0\1\7\1\0\15\7\2\0"+ + "\2\7\11\0\1\7\1\0\6\7\1\u0786\1\u07d8\6\0"+ + "\3\7\1\0\1\7\1\0\1\7\1\0\15\7\2\0"+ + "\2\7\11\0\1\7\1\0\6\7\1\0\1\7\6\0"+ + "\3\7\1\0\1\7\1\0\1\7\1\0\4\7\1\u07d9"+ + "\10\7\2\0\2\7\11\0\1\7\1\0\6\7\1\0"+ + "\1\7\6\0\3\7\1\0\1\7\1\0\1\7\1\0"+ + "\1\7\1\u07da\13\7\2\0\2\7\11\0\1\7\1\0"+ + "\6\7\1\0\1\7\6\0\3\7\1\0\1\7\1\0"+ + "\1\7\1\0\3\7\1\u07db\11\7\2\0\2\7\11\0"+ + "\1\7\1\0\5\7\1\u07dc\1\0\1\7\6\0\3\7"+ + "\1\0\1\7\1\0\1\7\1\0\15\7\2\0\2\7"+ + "\11\0\1\7\1\0\6\7\1\0\1\7\6\0\3\7"+ + "\1\0\1\7\1\0\1\7\1\0\4\7\1\u07dd\10\7"+ + "\2\0\2\7\11\0\1\7\1\0\1\u07de\5\7\1\0"+ + "\1\7\6\0\3\7\1\0\1\7\1\0\1\7\1\0"+ + "\15\7\2\0\2\7\11\0\1\u07df\1\0\6\7\1\0"+ + "\1\7\6\0\3\7\1\0\1\7\1\0\1\7\1\0"+ + "\15\7\2\0\2\7\11\0\1\7\1\0\6\7\1\0"+ + "\1\7\6\0\3\7\1\0\1\7\1\u06e2\1\u073c\1\0"+ + "\15\7\2\0\2\7\11\0\1\u07e0\1\0\4\7\1\u07e1"+ + "\1\7\1\0\1\7\6\0\3\7\1\0\1\7\1\0"+ + "\1\7\1\0\15\7\2\0\2\7\11\0\1\7\1\0"+ + "\6\7\1\0\1\7\6\0\1\u07e2\2\7\1\0\1\7"+ + "\1\0\1\7\1\0\3\7\1\u07e3\11\7\2\0\2\7"+ + "\11\0\1\7\1\0\1\u07e4\5\7\1\0\1\7\6\0"+ + "\3\7\1\0\1\7\1\0\1\7\1\0\15\7\2\0"+ + "\2\7\11\0\1\7\1\0\6\7\1\0\1\7\6\0"+ + "\3\7\1\0\1\7\1\0\1\7\1\0\5\7\1\u07e5"+ + "\7\7\2\0\2\7\11\0\1\7\1\0\1\7\1\u07e6"+ + "\4\7\1\0\1\7\6\0\3\7\1\0\1\7\1\0"+ + "\1\7\1\0\15\7\2\0\2\7\11\0\1\7\1\0"+ + "\4\7\1\u04ca\1\7\1\0\1\7\6\0\3\7\1\0"+ + "\1\7\1\0\1\7\1\u06a0\1\u06fa\14\7\2\0\2\7"+ + "\11\0\1\7\1\0\6\7\1\0\1\7\6\0\3\7"+ + "\1\0\1\7\1\0\1\7\1\0\4\7\1\u07e7\10\7"+ + "\2\0\2\7\11\0\1\u07e8\1\0\6\7\1\0\1\7"+ + "\6\0\3\7\1\0\1\7\1\0\1\7\1\0\15\7"+ + "\2\0\2\7\11\0\1\7\1\0\5\7\1\u07e9\1\0"+ + "\1\7\6\0\3\7\1\0\1\7\1\0\1\7\1\0"+ + "\15\7\2\0\2\7\11\0\1\7\1\0\6\7\1\0"+ + "\1\7\6\0\3\7\1\0\1\7\1\0\1\7\1\0"+ + "\10\7\1\u07ea\4\7\2\0\2\7\11\0\1\7\1\0"+ + "\6\7\1\0\1\7\6\0\1\u07eb\2\7\1\0\1\7"+ + "\1\0\1\7\1\0\15\7\2\0\2\7\11\0\1\7"+ + "\1\0\2\7\1\u07ec\3\7\1\0\1\7\6\0\3\7"+ + "\1\0\1\7\1\0\1\7\1\0\15\7\2\0\2\7"+ + "\11\0\1\7\1\0\6\7\1\0\1\7\6\0\3\7"+ + "\1\0\1\7\1\0\1\7\1\0\4\7\1\u07ed\10\7"+ + "\2\0\2\7\11\0\1\7\1\0\6\7\1\0\1\7"+ + "\6\0\3\7\1\0\1\7\1\0\1\7\1\u079c\1\u07ee"+ + "\14\7\44\0\2\u07ef\65\0\1\u07f0\47\0\1\u07f1\37\0"+ + "\1\u07f2\110\0\2\u07f3\35\0\1\u07f4\114\0\1\u07f5\30\0"+ + "\1\u07f6\75\0\1\u07f7\100\0\1\u07f8\42\0\1\u07f9\45\0"+ + "\1\u07fa\55\0\1\u050c\57\0\1\u07fb\116\0\1\u07fc\44\0"+ + "\1\u07fd\44\0\1\u07fe\23\0\2\u07ff\40\0\1\u0800\61\0"+ + "\1\u024e\60\0\1\u0801\75\0\1\u0802\57\0\1\u0803\50\0"+ + "\1\u0804\60\0\1\u0526\56\0\1\u0569\65\0\1\u0805\52\0"+ + "\1\u0806\110\0\2\u0807\41\0\2\u0808\56\0\1\u0809\53\0"+ + "\1\u080a\120\0\1\u080b\51\0\2\u06ea\62\0\1\u080c\32\0"+ + "\1\u080d\67\0\2\u080e\50\0\1\u080f\120\0\1\u077b\54\0"+ + "\1\u0810\35\0\1\u0811\24\0\1\u0812\46\0\1\u0813\43\0"+ + "\1\u0814\62\0\1\u07f3\60\0\1\u0815\100\0\1\u0816\42\0"+ + "\1\u0817\112\0\1\u075b\55\0\2\u0818\51\0\1\u0819\47\0"+ + "\1\u081a\112\0\1\u081b\32\0\2\u081c\60\0\2\u081d\77\0"+ + "\2\u081e\31\0\1\u05e3\61\0\1\u081f\120\0\1\u024e\31\0"+ + "\1\u0820\111\0\1\u06c3\51\0\2\u0821\33\0\1\u0822\110\0"+ + "\1\u0823\52\0\1\u0824\37\0\1\u0825\63\0\1\u0826\61\0"+ + "\1\u0827\106\0\2\u0828\37\0\1\u0829\63\0\2\u082a\70\0"+ + "\1\u082b\61\0\1\u061a\70\0\2\u082c\52\0\1\u0791\67\0"+ + "\2\u082d\60\0\2\u082e\53\0\1\u082f\45\0\1\u0830\61\0"+ + "\1\u0831\106\0\1\u0832\34\0\1\u0833\77\0\1\u0834\56\0"+ + "\1\u024e\50\0\2\u0835\52\0\1\u0836\66\0\1\u077d\37\0"+ + "\2\7\11\0\1\7\1\0\6\7\1\0\1\7\6\0"+ + "\3\7\1\0\1\7\1\0\1\7\1\u07ef\1\u0837\14\7"+ + "\2\0\2\7\11\0\1\7\1\0\6\7\1\0\1\7"+ + "\6\0\3\7\1\0\1\7\1\0\1\7\1\0\4\7"+ + "\1\u0838\10\7\2\0\2\7\11\0\1\7\1\0\6\7"+ + "\1\0\1\7\6\0\2\7\1\u0839\1\0\1\7\1\0"+ + "\1\7\1\0\15\7\2\0\2\7\11\0\1\u083a\1\0"+ + "\6\7\1\0\1\7\6\0\3\7\1\0\1\7\1\0"+ + "\1\7\1\0\15\7\2\0\2\7\11\0\1\7\1\0"+ + "\6\7\1\0\1\7\6\0\3\7\1\0\1\7\1\0"+ + "\1\7\1\u07f3\1\u083b\14\7\2\0\2\7\11\0\1\7"+ + "\1\0\2\7\1\u083c\3\7\1\0\1\7\6\0\3\7"+ + "\1\0\1\7\1\0\1\7\1\0\15\7\2\0\2\7"+ + "\11\0\1\7\1\0\4\7\1\u083d\1\7\1\0\1\7"+ + "\6\0\3\7\1\0\1\7\1\0\1\7\1\0\15\7"+ + "\2\0\2\7\11\0\1\7\1\0\6\7\1\0\1\7"+ + "\6\0\2\7\1\u083e\1\0\1\7\1\0\1\7\1\0"+ + "\15\7\2\0\2\7\11\0\1\7\1\0\6\7\1\0"+ + "\1\7\6\0\3\7\1\0\1\7\1\0\1\7\1\0"+ + "\11\7\1\u083f\3\7\2\0\2\7\11\0\1\7\1\0"+ + "\6\7\1\0\1\7\6\0\2\7\1\u0840\1\0\1\7"+ + "\1\0\1\7\1\0\15\7\2\0\2\7\11\0\1\7"+ + "\1\0\4\7\1\u0841\1\7\1\0\1\7\6\0\3\7"+ + "\1\0\1\7\1\0\1\7\1\0\15\7\2\0\2\7"+ + "\11\0\1\7\1\0\1\u056f\5\7\1\0\1\7\6\0"+ + "\3\7\1\0\1\7\1\0\1\7\1\0\15\7\2\0"+ + "\2\7\11\0\1\u0842\1\0\6\7\1\0\1\7\6\0"+ + "\3\7\1\0\1\7\1\0\1\7\1\0\15\7\2\0"+ + "\2\7\11\0\1\7\1\0\6\7\1\0\1\7\6\0"+ + "\3\7\1\0\1\7\1\0\1\7\1\0\5\7\1\u0843"+ + "\7\7\2\0\2\7\11\0\1\7\1\0\6\7\1\0"+ + "\1\7\6\0\1\u0844\2\7\1\0\1\7\1\0\1\7"+ + "\1\0\15\7\2\0\2\7\11\0\1\7\1\0\1\7"+ + "\1\u0845\4\7\1\0\1\7\6\0\3\7\1\0\1\7"+ + "\1\0\1\7\1\u07ff\1\u0846\14\7\2\0\2\7\11\0"+ + "\1\7\1\0\5\7\1\u0847\1\0\1\7\6\0\3\7"+ + "\1\0\1\7\1\0\1\7\1\0\15\7\2\0\2\7"+ + "\11\0\1\7\1\0\6\7\1\0\1\7\6\0\2\7"+ + "\1\u0848\1\0\1\7\1\0\1\7\1\0\15\7\2\0"+ + "\2\7\11\0\1\7\1\0\6\7\1\0\1\7\6\0"+ + "\1\u0849\2\7\1\0\1\7\1\0\1\7\1\0\15\7"+ + "\2\0\2\7\11\0\1\7\1\0\5\7\1\u084a\1\0"+ + "\1\7\6\0\3\7\1\0\1\7\1\0\1\7\1\0"+ + "\15\7\2\0\2\7\11\0\1\7\1\0\4\7\1\u0589"+ + "\1\7\1\0\1\7\6\0\3\7\1\0\1\7\1\0"+ + "\1\7\1\0\15\7\2\0\2\7\11\0\1\7\1\0"+ + "\1\7\1\u05cc\4\7\1\0\1\7\6\0\3\7\1\0"+ + "\1\7\1\0\1\7\1\0\15\7\2\0\2\7\11\0"+ + "\1\7\1\0\5\7\1\u084b\1\0\1\7\6\0\3\7"+ + "\1\0\1\7\1\0\1\7\1\0\15\7\2\0\2\7"+ + "\11\0\1\u084c\1\0\6\7\1\0\1\7\6\0\3\7"+ + "\1\0\1\7\1\0\1\7\1\0\15\7\2\0\2\7"+ + "\11\0\1\7\1\0\6\7\1\0\1\7\6\0\3\7"+ + "\1\0\1\7\1\0\1\7\1\u0807\1\u084d\14\7\2\0"+ + "\2\7\11\0\1\7\1\0\6\7\1\u0808\1\u084e\6\0"+ + "\3\7\1\0\1\7\1\0\1\7\1\0\15\7\2\0"+ + "\2\7\11\0\1\7\1\0\4\7\1\u084f\1\7\1\0"+ + "\1\7\6\0\3\7\1\0\1\7\1\0\1\7\1\0"+ + "\15\7\2\0\2\7\11\0\1\u0850\1\0\6\7\1\0"+ + "\1\7\6\0\3\7\1\0\1\7\1\0\1\7\1\0"+ + "\15\7\2\0\2\7\11\0\1\7\1\0\6\7\1\0"+ + "\1\7\6\0\3\7\1\0\1\7\1\0\1\7\1\0"+ + "\7\7\1\u0851\5\7\2\0\2\7\11\0\1\7\1\0"+ + "\6\7\1\0\1\7\6\0\3\7\1\0\1\7\1\0"+ + "\1\7\1\u06ea\1\u0744\14\7\2\0\2\7\11\0\1\7"+ + "\1\0\6\7\1\0\1\7\6\0\3\7\1\0\1\7"+ + "\1\0\1\7\1\0\1\7\1\u0852\13\7\2\0\2\7"+ + "\11\0\1\7\1\0\1\u0853\5\7\1\0\1\7\6\0"+ + "\3\7\1\0\1\7\1\0\1\7\1\0\15\7\2\0"+ + "\2\7\11\0\1\7\1\0\6\7\1\u080e\1\u0854\6\0"+ + "\3\7\1\0\1\7\1\0\1\7\1\0\15\7\2\0"+ + "\2\7\11\0\1\201\1\0\6\7\1\0\1\7\6\0"+ + "\3\7\1\0\1\7\1\0\1\7\1\0\15\7\2\0"+ + "\2\7\11\0\1\7\1\0\6\7\1\0\1\7\6\0"+ + "\3\7\1\0\1\7\1\0\1\7\1\0\7\7\1\u07cd"+ + "\5\7\2\0\2\7\11\0\1\7\1\0\6\7\1\0"+ + "\1\7\6\0\3\7\1\0\1\7\1\0\1\7\1\0"+ + "\2\7\1\u0855\12\7\2\0\2\7\11\0\1\7\1\0"+ + "\4\7\1\u0856\1\7\1\0\1\7\6\0\3\7\1\0"+ + "\1\7\1\0\1\7\1\0\3\7\1\u0857\11\7\2\0"+ + "\2\7\11\0\1\7\1\0\6\7\1\0\1\7\6\0"+ + "\1\u0858\2\7\1\0\1\7\1\0\1\7\1\0\15\7"+ + "\2\0\2\7\11\0\1\7\1\0\1\u0859\5\7\1\0"+ + "\1\7\6\0\3\7\1\0\1\7\1\0\1\7\1\0"+ + "\15\7\2\0\2\7\11\0\1\7\1\0\1\7\1\u083b"+ + "\4\7\1\0\1\7\6\0\3\7\1\0\1\7\1\0"+ + "\1\7\1\0\15\7\2\0\2\7\11\0\1\7\1\0"+ + "\1\u085a\5\7\1\0\1\7\6\0\3\7\1\0\1\7"+ + "\1\0\1\7\1\0\15\7\2\0\2\7\11\0\1\7"+ + "\1\0\6\7\1\0\1\7\6\0\1\7\1\u085b\1\7"+ + "\1\0\1\7\1\0\1\7\1\0\15\7\2\0\2\7"+ + "\11\0\1\7\1\0\1\u085c\5\7\1\0\1\7\6\0"+ + "\3\7\1\0\1\7\1\0\1\7\1\0\15\7\2\0"+ + "\2\7\11\0\1\7\1\0\6\7\1\0\1\7\6\0"+ + "\3\7\1\0\1\7\1\0\1\7\1\0\3\7\1\u03de"+ + "\11\7\2\0\2\7\11\0\1\7\1\0\6\7\1\0"+ + "\1\7\6\0\3\7\1\0\1\7\1\0\1\7\1\u0818"+ + "\1\u085d\14\7\2\0\2\7\11\0\1\7\1\0\6\7"+ + "\1\0\1\7\6\0\1\u085e\2\7\1\0\1\7\1\0"+ + "\1\7\1\0\15\7\2\0\2\7\11\0\1\7\1\0"+ + "\4\7\1\u085f\1\7\1\0\1\7\6\0\3\7\1\0"+ + "\1\7\1\0\1\7\1\0\15\7\2\0\2\7\11\0"+ + "\1\7\1\0\6\7\1\0\1\7\6\0\3\7\1\0"+ + "\1\7\1\0\1\7\1\0\7\7\1\u0860\5\7\2\0"+ + "\2\7\11\0\1\7\1\0\6\7\1\u081c\1\u0861\6\0"+ + "\3\7\1\0\1\7\1\0\1\7\1\0\15\7\2\0"+ + "\2\7\11\0\1\7\1\0\6\7\1\u081d\1\u0862\6\0"+ + "\3\7\1\0\1\7\1\0\1\7\1\0\15\7\2\0"+ + "\2\7\11\0\1\7\1\0\6\7\1\0\1\7\6\0"+ + "\3\7\1\0\1\7\1\0\1\7\1\u081e\1\u0863\14\7"+ + "\2\0\2\7\11\0\1\u0644\1\0\6\7\1\0\1\7"+ + "\6\0\3\7\1\0\1\7\1\0\1\7\1\0\15\7"+ + "\2\0\2\7\11\0\1\u0864\1\0\6\7\1\0\1\7"+ + "\6\0\3\7\1\0\1\7\1\0\1\7\1\0\15\7"+ + "\2\0\2\7\11\0\1\7\1\0\6\7\1\0\1\7"+ + "\6\0\3\7\1\0\1\7\1\0\1\7\1\0\7\7"+ + "\1\273\5\7\2\0\2\7\11\0\1\7\1\0\5\7"+ + "\1\u0865\1\0\1\7\6\0\3\7\1\0\1\7\1\0"+ + "\1\7\1\0\15\7\2\0\2\7\11\0\1\7\1\0"+ + "\6\7\1\0\1\7\6\0\3\7\1\0\1\7\1\0"+ + "\1\7\1\0\7\7\1\u071d\5\7\2\0\2\7\11\0"+ + "\1\7\1\0\6\7\1\0\1\7\6\0\3\7\1\0"+ + "\1\7\1\0\1\7\1\u0821\1\u0866\14\7\2\0\2\7"+ + "\11\0\1\7\1\0\1\u0867\5\7\1\0\1\7\6\0"+ + "\3\7\1\0\1\7\1\0\1\7\1\0\15\7\2\0"+ + "\2\7\11\0\1\7\1\0\6\7\1\0\1\7\6\0"+ + "\3\7\1\0\1\7\1\0\1\7\1\0\1\7\1\u0868"+ + "\13\7\2\0\2\7\11\0\1\7\1\0\6\7\1\0"+ + "\1\7\6\0\2\7\1\u0869\1\0\1\7\1\0\1\7"+ + "\1\0\15\7\2\0\2\7\11\0\1\u086a\1\0\6\7"+ + "\1\0\1\7\6\0\3\7\1\0\1\7\1\0\1\7"+ + "\1\0\15\7\2\0\2\7\11\0\1\7\1\0\1\u086b"+ + "\5\7\1\0\1\7\6\0\3\7\1\0\1\7\1\0"+ + "\1\7\1\0\15\7\2\0\2\7\11\0\1\7\1\0"+ + "\1\u086c\5\7\1\0\1\7\6\0\3\7\1\0\1\7"+ + "\1\0\1\7\1\0\15\7\2\0\2\7\11\0\1\7"+ + "\1\0\6\7\1\0\1\7\6\0\3\7\1\0\1\7"+ + "\1\0\1\7\1\u0828\1\u086d\14\7\2\0\2\7\11\0"+ + "\1\7\1\0\4\7\1\u086e\1\7\1\0\1\7\6\0"+ + "\3\7\1\0\1\7\1\0\1\7\1\0\15\7\2\0"+ + "\2\7\11\0\1\7\1\0\6\7\1\u082a\1\u086f\6\0"+ + "\3\7\1\0\1\7\1\0\1\7\1\0\15\7\2\0"+ + "\2\7\11\0\1\7\1\0\6\7\1\0\1\7\6\0"+ + "\1\u0870\2\7\1\0\1\7\1\0\1\7\1\0\15\7"+ + "\2\0\2\7\11\0\1\7\1\0\6\7\1\0\1\7"+ + "\6\0\1\u067b\2\7\1\0\1\7\1\0\1\7\1\0"+ + "\15\7\2\0\2\7\11\0\1\7\1\0\6\7\1\0"+ + "\1\7\6\0\3\7\1\0\1\7\1\0\1\7\1\u082c"+ + "\1\u0871\14\7\2\0\2\7\11\0\1\7\1\0\6\7"+ + "\1\0\1\7\6\0\1\7\1\u07e3\1\7\1\0\1\7"+ + "\1\0\1\7\1\0\15\7\2\0\2\7\11\0\1\7"+ + "\1\0\6\7\1\0\1\7\6\0\3\7\1\0\1\7"+ + "\1\0\1\7\1\u082d\1\u0872\14\7\2\0\2\7\11\0"+ + "\1\7\1\0\6\7\1\0\1\7\6\0\3\7\1\0"+ + "\1\7\1\0\1\7\1\u082e\1\u0873\14\7\2\0\2\7"+ + "\11\0\1\7\1\0\6\7\1\0\1\7\6\0\2\7"+ + "\1\u0874\1\0\1\7\1\0\1\7\1\0\15\7\2\0"+ + "\2\7\11\0\1\7\1\0\4\7\1\u0875\1\7\1\0"+ + "\1\7\6\0\3\7\1\0\1\7\1\0\1\7\1\0"+ + "\15\7\2\0\2\7\11\0\1\7\1\0\4\7\1\u0876"+ + "\1\7\1\0\1\7\6\0\3\7\1\0\1\7\1\0"+ + "\1\7\1\0\15\7\2\0\2\7\11\0\1\7\1\0"+ + "\6\7\1\0\1\7\6\0\3\7\1\0\1\7\1\0"+ + "\1\7\1\0\3\7\1\u0877\11\7\2\0\2\7\11\0"+ + "\1\7\1\0\4\7\1\u0878\1\7\1\0\1\7\6\0"+ + "\3\7\1\0\1\7\1\0\1\7\1\0\15\7\2\0"+ + "\2\7\11\0\1\7\1\0\6\7\1\0\1\7\6\0"+ + "\3\7\1\0\1\u0879\1\0\1\7\1\0\15\7\2\0"+ + "\2\7\11\0\1\7\1\0\6\7\1\0\1\7\6\0"+ + "\1\7\1\273\1\7\1\0\1\7\1\0\1\7\1\0"+ + "\15\7\2\0\2\7\11\0\1\7\1\0\6\7\1\u0835"+ + "\1\u087a\6\0\3\7\1\0\1\7\1\0\1\7\1\0"+ + "\15\7\2\0\2\7\11\0\1\7\1\0\1\u087b\5\7"+ + "\1\0\1\7\6\0\3\7\1\0\1\7\1\0\1\7"+ + "\1\0\15\7\2\0\2\7\11\0\1\7\1\0\5\7"+ + "\1\u07cf\1\0\1\7\6\0\3\7\1\0\1\7\1\0"+ + "\1\7\1\0\15\7\50\0\1\u087c\34\0\1\u087d\73\0"+ + "\1\u041e\61\0\1\u087e\41\0\1\u087f\70\0\1\u0880\76\0"+ + "\1\u0881\45\0\2\u0882\53\0\1\u0883\56\0\1\u0884\65\0"+ + "\1\u0885\60\0\1\u0886\115\0\1\u0887\51\0\2\u0888\33\0"+ + "\1\u0889\57\0\1\u088a\101\0\1\u088b\41\0\1\u088c\113\0"+ + "\1\u0237\36\0\1\u088d\62\0\2\u088e\52\0\1\u088f\63\0"+ + "\1\u0237\110\0\1\u0890\60\0\1\u0891\31\0\1\u0892\66\0"+ + "\1\u0893\111\0\1\u0894\22\0\1\u0895\67\0\1\u0896\77\0"+ + "\1\u0897\37\0\1\u01dc\115\0\1\u01da\31\0\1\u0898\106\0"+ + "\1\u0899\50\0\1\u089a\66\0\2\u089b\53\0\1\u089c\37\0"+ + "\1\u089d\61\0\1\u089e\61\0\1\u089f\63\0\1\u08a0\61\0"+ + "\1\u08a1\103\0\1\u08a2\57\0\1\u08a3\41\0\1\u08a4\65\0"+ + "\1\u08a5\73\0\1\u0237\72\0\1\u08a6\51\0\1\u08a7\42\0"+ + "\1\u08a8\61\0\1\u08a9\101\0\1\u08aa\44\0\1\u08ab\103\0"+ + "\2\u08ac\31\0\1\u08ad\122\0\1\u08ae\41\0\1\u08af\43\0"+ + "\1\u08b0\107\0\1\u08b1\32\0\1\u08b2\77\0\1\u08b3\43\0"+ + "\1\u08b4\110\0\1\u08b5\33\0\1\u08b6\105\0\2\u08b7\64\0"+ + "\1\u08b8\50\0\1\u08b9\77\0\1\u08ba\43\0\1\u08bb\37\0"+ + "\1\u08bc\46\0\1\u08bd\61\0\2\7\11\0\1\7\1\0"+ + "\6\7\1\0\1\7\6\0\3\7\1\0\1\7\1\0"+ + "\1\7\1\0\3\7\1\u08be\11\7\2\0\2\7\11\0"+ + "\1\7\1\0\4\7\1\u08bf\1\7\1\0\1\7\6\0"+ + "\3\7\1\0\1\7\1\0\1\7\1\0\15\7\2\0"+ + "\2\7\11\0\1\7\1\0\6\7\1\0\1\7\6\0"+ + "\1\u0458\2\7\1\0\1\7\1\0\1\7\1\0\15\7"+ + "\2\0\2\7\11\0\1\7\1\0\6\7\1\0\1\7"+ + "\6\0\1\u08c0\2\7\1\0\1\7\1\0\1\7\1\0"+ + "\15\7\2\0\2\7\11\0\1\u08c1\1\0\6\7\1\0"+ + "\1\7\6\0\3\7\1\0\1\7\1\0\1\7\1\0"+ + "\15\7\2\0\2\7\11\0\1\7\1\0\5\7\1\u08c2"+ + "\1\0\1\7\6\0\3\7\1\0\1\7\1\0\1\7"+ + "\1\0\15\7\2\0\2\7\11\0\1\7\1\0\6\7"+ + "\1\u0882\1\u08c3\6\0\3\7\1\0\1\7\1\0\1\7"+ + "\1\0\15\7\2\0\2\7\11\0\1\7\1\0\1\7"+ + "\1\u08c4\4\7\1\0\1\7\6\0\3\7\1\0\1\7"+ + "\1\0\1\7\1\0\15\7\2\0\2\7\11\0\1\u08c5"+ + "\1\0\6\7\1\0\1\7\6\0\3\7\1\0\1\7"+ + "\1\0\1\7\1\0\15\7\2\0\2\7\11\0\1\7"+ + "\1\0\2\7\1\u08c6\3\7\1\0\1\7\6\0\3\7"+ + "\1\0\1\7\1\0\1\7\1\0\15\7\2\0\2\7"+ + "\11\0\1\7\1\0\1\7\1\u08c7\4\7\1\0\1\7"+ + "\6\0\3\7\1\0\1\7\1\0\1\7\1\0\15\7"+ + "\2\0\2\7\11\0\1\7\1\0\6\7\1\0\1\7"+ + "\6\0\3\7\1\0\1\7\1\0\1\7\1\0\7\7"+ + "\1\u08c8\5\7\2\0\2\7\11\0\1\7\1\0\6\7"+ + "\1\0\1\7\6\0\3\7\1\0\1\7\1\0\1\7"+ + "\1\u0888\1\u08c9\14\7\2\0\2\7\11\0\1\7\1\0"+ + "\1\u08ca\5\7\1\0\1\7\6\0\3\7\1\0\1\7"+ + "\1\0\1\7\1\0\15\7\2\0\2\7\11\0\1\u08cb"+ + "\1\0\6\7\1\0\1\7\6\0\3\7\1\0\1\7"+ + "\1\0\1\7\1\0\15\7\2\0\2\7\11\0\1\7"+ + "\1\0\6\7\1\0\1\7\6\0\1\u08cc\2\7\1\0"+ + "\1\7\1\0\1\7\1\0\15\7\2\0\2\7\11\0"+ + "\1\u08cd\1\0\6\7\1\0\1\7\6\0\3\7\1\0"+ + "\1\7\1\0\1\7\1\0\15\7\2\0\2\7\11\0"+ + "\1\7\1\0\5\7\1\301\1\0\1\7\6\0\3\7"+ + "\1\0\1\7\1\0\1\7\1\0\15\7\2\0\2\7"+ + "\11\0\1\7\1\0\6\7\1\u088e\1\u08ce\6\0\3\7"+ + "\1\0\1\7\1\0\1\7\1\0\15\7\2\0\2\7"+ + "\11\0\1\7\1\0\1\u08cf\5\7\1\0\1\7\6\0"+ + "\3\7\1\0\1\7\1\0\1\7\1\0\15\7\2\0"+ + "\2\7\11\0\1\7\1\0\2\7\1\326\3\7\1\0"+ + "\1\7\6\0\3\7\1\0\1\7\1\0\1\7\1\0"+ + "\15\7\2\0\2\7\11\0\1\7\1\0\6\7\1\0"+ + "\1\7\6\0\3\7\1\0\1\7\1\0\1\7\1\0"+ + "\3\7\1\u08d0\11\7\2\0\2\7\11\0\1\7\1\0"+ + "\6\7\1\0\1\7\6\0\3\7\1\0\1\7\1\0"+ + "\1\7\1\0\2\7\1\u08d1\12\7\2\0\2\7\11\0"+ + "\1\7\1\0\1\u08d2\5\7\1\0\1\7\6\0\3\7"+ + "\1\0\1\7\1\0\1\7\1\0\15\7\2\0\2\7"+ + "\11\0\1\7\1\0\5\7\1\u08d3\1\0\1\7\6\0"+ + "\3\7\1\0\1\7\1\0\1\7\1\0\15\7\2\0"+ + "\2\7\11\0\1\7\1\0\6\7\1\0\1\7\6\0"+ + "\3\7\1\0\1\7\1\0\1\7\1\0\7\7\1\u08d4"+ + "\5\7\2\0\2\7\11\0\1\u08d5\1\0\6\7\1\0"+ + "\1\7\6\0\3\7\1\0\1\7\1\0\1\7\1\0"+ + "\15\7\2\0\2\7\11\0\1\7\1\0\4\7\1\u08d6"+ + "\1\7\1\0\1\7\6\0\3\7\1\0\1\7\1\0"+ + "\1\7\1\0\15\7\2\0\2\7\11\0\1\7\1\0"+ + "\6\7\1\0\1\7\6\0\3\7\1\0\1\u08d7\1\0"+ + "\1\7\1\0\15\7\2\0\2\7\11\0\1\7\1\0"+ + "\1\u01dd\5\7\1\0\1\7\6\0\3\7\1\0\1\7"+ + "\1\0\1\7\1\0\15\7\2\0\2\7\11\0\1\7"+ + "\1\0\4\7\1\u08d8\1\7\1\0\1\7\6\0\3\7"+ + "\1\0\1\7\1\0\1\7\1\0\15\7\2\0\2\7"+ + "\11\0\1\7\1\0\6\7\1\0\1\7\6\0\3\7"+ + "\1\0\1\7\1\0\1\7\1\0\3\7\1\u08d9\11\7"+ + "\2\0\2\7\11\0\1\7\1\0\6\7\1\0\1\7"+ + "\6\0\2\7\1\u08da\1\0\1\7\1\0\1\7\1\0"+ + "\15\7\2\0\2\7\11\0\1\7\1\0\6\7\1\0"+ + "\1\7\6\0\3\7\1\0\1\7\1\0\1\7\1\u089b"+ + "\1\u08db\14\7\2\0\2\7\11\0\1\7\1\0\6\7"+ + "\1\0\1\7\6\0\2\7\1\u08dc\1\0\1\7\1\0"+ + "\1\7\1\0\15\7\2\0\2\7\11\0\1\u08dd\1\0"+ + "\6\7\1\0\1\7\6\0\3\7\1\0\1\7\1\0"+ + "\1\7\1\0\15\7\2\0\2\7\11\0\1\u08de\1\0"+ + "\6\7\1\0\1\7\6\0\3\7\1\0\1\7\1\0"+ + "\1\7\1\0\15\7\2\0\2\7\11\0\1\u08df\1\0"+ + "\6\7\1\0\1\7\6\0\3\7\1\0\1\7\1\0"+ + "\1\7\1\0\15\7\2\0\2\7\11\0\1\7\1\0"+ + "\1\u08e0\5\7\1\0\1\7\6\0\3\7\1\0\1\7"+ + "\1\0\1\7\1\0\15\7\2\0\2\7\11\0\1\7"+ + "\1\0\1\u08e1\5\7\1\0\1\7\6\0\3\7\1\0"+ + "\1\7\1\0\1\7\1\0\15\7\2\0\2\7\11\0"+ + "\1\7\1\0\6\7\1\0\1\7\6\0\3\7\1\0"+ + "\1\u08e2\1\0\1\7\1\0\15\7\2\0\2\7\11\0"+ + "\1\7\1\0\6\7\1\0\1\7\6\0\2\7\1\u08e3"+ + "\1\0\1\7\1\0\1\7\1\0\15\7\2\0\2\7"+ + "\11\0\1\7\1\0\1\346\5\7\1\0\1\7\6\0"+ + "\3\7\1\0\1\7\1\0\1\7\1\0\15\7\2\0"+ + "\2\7\11\0\1\7\1\0\4\7\1\u08e4\1\7\1\0"+ + "\1\7\6\0\3\7\1\0\1\7\1\0\1\7\1\0"+ + "\15\7\2\0\2\7\11\0\1\7\1\0\6\7\1\0"+ + "\1\7\6\0\1\326\2\7\1\0\1\7\1\0\1\7"+ + "\1\0\15\7\2\0\2\7\11\0\1\7\1\0\6\7"+ + "\1\0\1\7\6\0\3\7\1\0\1\7\1\0\1\7"+ + "\1\0\1\7\1\u08e5\13\7\2\0\2\7\11\0\1\7"+ + "\1\0\6\7\1\0\1\7\6\0\1\7\1\u08e6\1\7"+ + "\1\0\1\7\1\0\1\7\1\0\15\7\2\0\2\7"+ + "\11\0\1\7\1\0\1\u08e7\5\7\1\0\1\7\6\0"+ + "\3\7\1\0\1\7\1\0\1\7\1\0\15\7\2\0"+ + "\2\7\11\0\1\7\1\0\1\u08e8\5\7\1\0\1\7"+ + "\6\0\3\7\1\0\1\7\1\0\1\7\1\0\15\7"+ + "\2\0\2\7\11\0\1\7\1\0\6\7\1\0\1\7"+ + "\6\0\2\7\1\u08e9\1\0\1\7\1\0\1\7\1\0"+ + "\15\7\2\0\2\7\11\0\1\7\1\0\3\7\1\u08ea"+ + "\2\7\1\0\1\7\6\0\3\7\1\0\1\7\1\0"+ + "\1\7\1\0\15\7\2\0\2\7\11\0\1\7\1\0"+ + "\6\7\1\0\1\7\6\0\3\7\1\0\1\7\1\0"+ + "\1\7\1\u08ac\1\u08eb\14\7\2\0\2\7\11\0\1\u08ec"+ + "\1\0\6\7\1\0\1\7\6\0\3\7\1\0\1\7"+ + "\1\0\1\7\1\0\15\7\2\0\2\7\11\0\1\7"+ + "\1\0\6\7\1\0\1\7\6\0\3\7\1\0\1\7"+ + "\1\0\1\7\1\0\11\7\1\u08ed\3\7\2\0\2\7"+ + "\11\0\1\7\1\0\6\7\1\0\1\7\6\0\1\7"+ + "\1\u08ee\1\7\1\0\1\7\1\0\1\7\1\0\15\7"+ + "\2\0\2\7\11\0\1\7\1\0\1\7\1\u08ef\4\7"+ + "\1\0\1\7\6\0\3\7\1\0\1\7\1\0\1\7"+ + "\1\0\15\7\2\0\2\7\11\0\1\7\1\0\6\7"+ + "\1\0\1\7\6\0\3\7\1\0\1\7\1\0\1\7"+ + "\1\0\1\7\1\u08f0\13\7\2\0\2\7\11\0\1\7"+ + "\1\0\1\u08f1\5\7\1\0\1\7\6\0\3\7\1\0"+ + "\1\7\1\0\1\7\1\0\15\7\2\0\2\7\11\0"+ + "\1\7\1\0\6\7\1\0\1\7\6\0\1\u08f2\2\7"+ + "\1\0\1\7\1\0\1\7\1\0\15\7\2\0\2\7"+ + "\11\0\1\7\1\0\1\u08f3\5\7\1\0\1\7\6\0"+ + "\3\7\1\0\1\7\1\0\1\7\1\0\15\7\2\0"+ + "\2\7\11\0\1\7\1\0\6\7\1\0\1\7\6\0"+ + "\3\7\1\0\1\7\1\0\1\7\1\0\1\7\1\u08f4"+ + "\13\7\2\0\2\7\11\0\1\7\1\0\1\7\1\u08f5"+ + "\4\7\1\0\1\7\6\0\3\7\1\0\1\7\1\0"+ + "\1\7\1\0\15\7\2\0\2\7\11\0\1\7\1\0"+ + "\6\7\1\0\1\7\6\0\3\7\1\0\1\7\1\0"+ + "\1\7\1\u08b7\1\u08f6\14\7\2\0\2\7\11\0\1\7"+ + "\1\0\6\7\1\0\1\7\6\0\3\7\1\0\1\7"+ + "\1\0\1\7\1\0\3\7\1\u08f7\11\7\2\0\2\7"+ + "\11\0\1\7\1\0\6\7\1\0\1\7\6\0\2\7"+ + "\1\u08f8\1\0\1\7\1\0\1\7\1\0\15\7\2\0"+ + "\2\7\11\0\1\7\1\0\6\7\1\0\1\7\6\0"+ + "\3\7\1\0\1\7\1\0\1\7\1\0\10\7\1\u08f9"+ + "\4\7\2\0\2\7\11\0\1\7\1\0\6\7\1\0"+ + "\1\7\6\0\2\7\1\u08fa\1\0\1\7\1\0\1\7"+ + "\1\0\15\7\2\0\2\7\11\0\1\u08fb\1\0\6\7"+ + "\1\0\1\7\6\0\3\7\1\0\1\7\1\0\1\7"+ + "\1\0\15\7\2\0\1\u08fc\1\7\11\0\1\7\1\0"+ + "\6\7\1\0\1\7\6\0\3\7\1\0\1\7\1\0"+ + "\1\7\1\0\15\7\24\0\1\u08fd\62\0\2\u08fe\60\0"+ + "\2\u08ff\70\0\1\u0900\43\0\1\u0901\57\0\1\u0902\63\0"+ + "\1\u0903\57\0\1\u0904\101\0\1\u0905\62\0\1\u0906\46\0"+ + "\1\u0907\77\0\1\u0908\37\0\1\u0909\57\0\1\u0145\112\0"+ + "\1\u090a\71\0\1\u090b\47\0\2\u090c\53\0\1\u0902\66\0"+ + "\2\u090d\53\0\1\u05df\46\0\1\u090e\106\0\1\u090f\54\0"+ + "\2\u0611\53\0\1\u0910\45\0\1\u0911\63\0\2\u0912\103\0"+ + "\1\u0913\32\0\1\u0914\114\0\1\u0237\55\0\1\u0915\31\0"+ + "\1\u0916\115\0\1\u0917\42\0\1\u0918\100\0\1\u0919\42\0"+ + "\1\u091a\44\0\1\u091b\61\0\1\u0918\3\0\1\u0918\11\0"+ + "\1\u091c\5\0\2\u0773\53\0\1\u091d\46\0\1\u091e\113\0"+ + "\1\u0201\26\0\1\u091f\114\0\1\u0237\22\0\1\u0920\65\0"+ + "\1\u01da\102\0\2\u0921\53\0\1\u0922\43\0\1\u0923\57\0"+ + "\1\u0924\111\0\1\u0925\50\0\1\u0926\46\0\1\u0927\53\0"+ + "\1\u0928\67\0\1\u0929\53\0\1\u092a\71\0\2\u092b\20\0"+ + "\1\u040d\4\0\1\u0533\22\0\1\u092c\61\0\1\u092d\106\0"+ + "\2\u01dc\54\0\1\u092e\42\0\1\u092f\61\0\1\u0930\67\0"+ + "\2\u0931\60\0\2\u0932\56\0\1\u0933\75\0\1\u0934\61\0"+ + "\1\u0935\24\0\2\7\11\0\1\7\1\0\5\7\1\u0936"+ + "\1\0\1\7\6\0\3\7\1\0\1\7\1\0\1\7"+ + "\1\0\15\7\2\0\2\7\11\0\1\7\1\0\6\7"+ + "\1\u08fe\1\u0937\6\0\3\7\1\0\1\7\1\0\1\7"+ + "\1\0\15\7\2\0\2\7\11\0\1\7\1\0\6\7"+ + "\1\u08ff\1\u0938\6\0\3\7\1\0\1\7\1\0\1\7"+ + "\1\0\15\7\2\0\2\7\11\0\1\7\1\0\6\7"+ + "\1\0\1\7\6\0\1\u0939\2\7\1\0\1\7\1\0"+ + "\1\7\1\0\15\7\2\0\2\7\11\0\1\7\1\0"+ + "\1\u093a\5\7\1\0\1\7\6\0\3\7\1\0\1\7"+ + "\1\0\1\7\1\0\15\7\2\0\2\7\11\0\1\7"+ + "\1\0\1\u093b\5\7\1\0\1\7\6\0\3\7\1\0"+ + "\1\7\1\0\1\7\1\0\15\7\2\0\2\7\11\0"+ + "\1\u093c\1\0\6\7\1\0\1\7\6\0\3\7\1\0"+ + "\1\7\1\0\1\7\1\0\15\7\2\0\2\7\11\0"+ + "\1\7\1\0\6\7\1\0\1\7\6\0\1\u093d\2\7"+ + "\1\0\1\7\1\0\1\7\1\0\15\7\2\0\2\7"+ + "\11\0\1\7\1\0\6\7\1\0\1\7\6\0\1\7"+ + "\1\u093e\1\7\1\0\1\7\1\0\1\7\1\0\15\7"+ + "\2\0\2\7\11\0\1\7\1\0\4\7\1\u093f\1\7"+ + "\1\0\1\7\6\0\3\7\1\0\1\7\1\0\1\7"+ + "\1\0\15\7\2\0\2\7\11\0\1\7\1\0\6\7"+ + "\1\0\1\7\6\0\3\7\1\0\1\u0940\1\0\1\7"+ + "\1\0\15\7\2\0\2\7\11\0\1\7\1\0\1\u0941"+ + "\5\7\1\0\1\7\6\0\3\7\1\0\1\7\1\0"+ + "\1\7\1\0\15\7\2\0\2\7\11\0\1\u0146\1\0"+ + "\6\7\1\0\1\7\6\0\3\7\1\0\1\7\1\0"+ + "\1\7\1\0\15\7\2\0\2\7\11\0\1\7\1\0"+ + "\6\7\1\0\1\7\6\0\3\7\1\0\1\7\1\0"+ + "\1\7\1\0\1\7\1\u0942\13\7\2\0\2\7\11\0"+ + "\1\7\1\0\6\7\1\0\1\7\6\0\3\7\1\0"+ + "\1\7\1\0\1\7\1\0\11\7\1\u0943\3\7\2\0"+ + "\2\7\11\0\1\7\1\0\6\7\1\0\1\7\6\0"+ + "\3\7\1\0\1\7\1\0\1\7\1\u090c\1\u0944\14\7"+ + "\2\0\2\7\11\0\1\7\1\0\6\7\1\0\1\7"+ + "\6\0\3\7\1\0\1\7\1\0\1\7\1\u090d\1\u0945"+ + "\14\7\2\0\2\7\11\0\1\7\1\0\6\7\1\0"+ + "\1\7\6\0\2\7\1\u0640\1\0\1\7\1\0\1\7"+ + "\1\0\15\7\2\0\2\7\11\0\1\7\1\0\5\7"+ + "\1\u0946\1\0\1\7\6\0\3\7\1\0\1\7\1\0"+ + "\1\7\1\0\15\7\2\0\2\7\11\0\1\7\1\0"+ + "\6\7\1\0\1\7\6\0\3\7\1\0\1\7\1\0"+ + "\1\7\1\0\4\7\1\u0947\10\7\2\0\2\7\11\0"+ + "\1\7\1\0\6\7\1\0\1\7\6\0\3\7\1\0"+ + "\1\7\1\0\1\7\1\u0611\1\u0672\14\7\2\0\2\7"+ + "\11\0\1\7\1\0\6\7\1\0\1\7\6\0\2\7"+ + "\1\u0948\1\0\1\7\1\0\1\7\1\0\15\7\2\0"+ + "\2\7\11\0\1\7\1\0\4\7\1\u0949\1\7\1\0"+ + "\1\7\6\0\3\7\1\0\1\7\1\0\1\7\1\0"+ + "\15\7\2\0\2\7\11\0\1\7\1\0\6\7\1\u0912"+ + "\1\u094a\6\0\3\7\1\0\1\7\1\0\1\7\1\0"+ + "\15\7\2\0\2\7\11\0\1\7\1\0\6\7\1\0"+ + "\1\7\6\0\3\7\1\0\1\7\1\0\1\7\1\0"+ + "\3\7\1\u094b\11\7\2\0\2\7\11\0\1\7\1\0"+ + "\2\7\1\u094c\3\7\1\0\1\7\6\0\3\7\1\0"+ + "\1\7\1\0\1\7\1\0\15\7\2\0\2\7\11\0"+ + "\1\7\1\0\6\7\1\0\1\7\6\0\3\7\1\0"+ + "\1\7\1\0\1\7\1\0\7\7\1\326\5\7\2\0"+ + "\2\7\11\0\1\7\1\0\6\7\1\0\1\7\6\0"+ + "\3\7\1\0\1\7\1\0\1\7\1\0\3\7\1\u094d"+ + "\11\7\2\0\2\7\11\0\1\7\1\0\1\7\1\u094e"+ + "\4\7\1\0\1\7\6\0\3\7\1\0\1\7\1\0"+ + "\1\7\1\0\15\7\2\0\2\7\11\0\1\7\1\0"+ + "\6\7\1\0\1\7\6\0\3\7\1\0\1\7\1\0"+ + "\1\7\1\0\7\7\1\u094f\5\7\2\0\2\7\11\0"+ + "\1\7\1\0\6\7\1\0\1\7\6\0\1\u0950\2\7"+ + "\1\0\1\7\1\0\1\7\1\0\15\7\2\0\2\7"+ + "\11\0\1\7\1\0\6\7\1\0\1\7\6\0\3\7"+ + "\1\0\1\7\1\0\1\7\1\0\7\7\1\u0951\5\7"+ + "\2\0\2\7\11\0\1\7\1\0\6\7\1\0\1\7"+ + "\6\0\1\u0952\2\7\1\0\1\7\1\0\1\7\1\0"+ + "\15\7\2\0\2\7\11\0\1\7\1\0\1\7\1\u0953"+ + "\4\7\1\0\1\7\6\0\3\7\1\0\1\7\1\0"+ + "\1\7\1\0\15\7\2\0\2\7\11\0\1\7\1\0"+ + "\1\7\1\u0950\3\7\1\u0950\1\0\1\7\6\0\1\7"+ + "\1\u0954\1\7\1\0\1\7\1\0\1\7\1\u0773\1\u07c5"+ + "\14\7\2\0\2\7\11\0\1\7\1\0\6\7\1\0"+ + "\1\7\6\0\2\7\1\u0955\1\0\1\7\1\0\1\7"+ + "\1\0\15\7\2\0\2\7\11\0\1\7\1\0\5\7"+ + "\1\u0956\1\0\1\7\6\0\3\7\1\0\1\7\1\0"+ + "\1\7\1\0\15\7\2\0\2\7\11\0\1\7\1\0"+ + "\6\7\1\0\1\7\6\0\3\7\1\0\1\7\1\0"+ + "\1\7\1\0\11\7\1\u0202\3\7\2\0\2\7\11\0"+ + "\1\7\1\0\6\7\1\0\1\7\6\0\3\7\1\0"+ + "\1\7\1\0\1\7\1\0\11\7\1\326\3\7\2\0"+ + "\2\7\11\0\1\7\1\0\1\u0957\5\7\1\0\1\7"+ + "\6\0\3\7\1\0\1\7\1\0\1\7\1\0\15\7"+ + "\2\0\2\7\11\0\1\7\1\0\4\7\1\u011c\1\7"+ + "\1\0\1\7\6\0\3\7\1\0\1\7\1\0\1\7"+ + "\1\0\15\7\2\0\2\7\11\0\1\7\1\0\6\7"+ + "\1\0\1\7\6\0\3\7\1\0\1\7\1\0\1\7"+ + "\1\u0921\1\u0958\14\7\2\0\2\7\11\0\1\7\1\0"+ + "\6\7\1\0\1\7\6\0\2\7\1\u0959\1\0\1\7"+ + "\1\0\1\7\1\0\15\7\2\0\2\7\11\0\1\7"+ + "\1\0\2\7\1\u095a\3\7\1\0\1\7\6\0\3\7"+ + "\1\0\1\7\1\0\1\7\1\0\15\7\2\0\2\7"+ + "\11\0\1\7\1\0\1\u095b\5\7\1\0\1\7\6\0"+ + "\3\7\1\0\1\7\1\0\1\7\1\0\15\7\2\0"+ + "\2\7\11\0\1\7\1\0\6\7\1\0\1\7\6\0"+ + "\3\7\1\0\1\7\1\0\1\7\1\0\2\7\1\u095c"+ + "\12\7\2\0\2\7\11\0\1\7\1\0\6\7\1\0"+ + "\1\7\6\0\1\7\1\u095d\1\7\1\0\1\7\1\0"+ + "\1\7\1\0\15\7\2\0\2\7\11\0\1\7\1\0"+ + "\4\7\1\u095e\1\7\1\0\1\7\6\0\3\7\1\0"+ + "\1\7\1\0\1\7\1\0\15\7\2\0\2\7\11\0"+ + "\1\u095f\1\0\6\7\1\0\1\7\6\0\3\7\1\0"+ + "\1\7\1\0\1\7\1\0\15\7\2\0\2\7\11\0"+ + "\1\7\1\0\4\7\1\u0960\1\7\1\0\1\7\6\0"+ + "\3\7\1\0\1\7\1\0\1\7\1\0\15\7\2\0"+ + "\2\7\11\0\1\u0961\1\0\6\7\1\0\1\7\6\0"+ + "\3\7\1\0\1\7\1\0\1\7\1\0\15\7\2\0"+ + "\2\7\11\0\1\7\1\0\6\7\1\u092b\1\u0962\6\0"+ + "\3\7\1\0\1\7\1\0\1\7\1\0\2\7\1\u0447"+ + "\4\7\1\u0596\5\7\2\0\2\7\11\0\1\u0963\1\0"+ + "\6\7\1\0\1\7\6\0\3\7\1\0\1\7\1\0"+ + "\1\7\1\0\15\7\2\0\2\7\11\0\1\u0964\1\0"+ + "\6\7\1\0\1\7\6\0\3\7\1\0\1\7\1\0"+ + "\1\7\1\0\15\7\2\0\2\7\11\0\1\7\1\0"+ + "\6\7\1\0\1\7\6\0\3\7\1\0\1\7\1\u01dc"+ + "\1\u01dd\1\0\15\7\2\0\2\7\11\0\1\7\1\0"+ + "\6\7\1\0\1\7\6\0\1\7\1\u0965\1\7\1\0"+ + "\1\7\1\0\1\7\1\0\15\7\2\0\2\7\11\0"+ + "\1\7\1\0\1\u0966\5\7\1\0\1\7\6\0\3\7"+ + "\1\0\1\7\1\0\1\7\1\0\15\7\2\0\2\7"+ + "\11\0\1\7\1\0\1\u0967\5\7\1\0\1\7\6\0"+ + "\3\7\1\0\1\7\1\0\1\7\1\0\15\7\2\0"+ + "\2\7\11\0\1\7\1\0\6\7\1\u0931\1\u0968\6\0"+ + "\3\7\1\0\1\7\1\0\1\7\1\0\15\7\2\0"+ + "\2\7\11\0\1\7\1\0\6\7\1\u0932\1\u0969\6\0"+ + "\3\7\1\0\1\7\1\0\1\7\1\0\15\7\2\0"+ + "\2\7\11\0\1\7\1\0\4\7\1\u096a\1\7\1\0"+ + "\1\7\6\0\3\7\1\0\1\7\1\0\1\7\1\0"+ + "\15\7\2\0\2\7\11\0\1\7\1\0\6\7\1\0"+ + "\1\7\6\0\2\7\1\u096b\1\0\1\7\1\0\1\7"+ + "\1\0\15\7\2\0\2\7\11\0\1\7\1\0\6\7"+ + "\1\0\1\7\6\0\2\7\1\u096c\1\0\1\7\1\0"+ + "\1\7\1\0\15\7\23\0\1\u096d\60\0\1\u096e\110\0"+ + "\1\u05fb\27\0\1\u096f\106\0\2\u0970\34\0\1\u024e\64\0"+ + "\1\u080e\56\0\1\u0971\33\0\1\u0972\24\0\1\u0973\67\0"+ + "\2\u0974\101\0\1\u0975\70\0\1\u0976\55\0\1\u0977\47\0"+ + "\1\u0978\37\0\1\u0979\101\0\1\u097a\103\0\1\u0237\25\0"+ + "\1\u097b\55\0\1\u097c\111\0\1\u097d\53\0\1\u097e\45\0"+ + "\2\u075b\52\0\1\u097f\61\0\1\u0980\57\0\1\u0981\63\0"+ + "\1\u0982\65\0\1\u055b\104\0\1\u0983\40\0\2\u0984\52\0"+ + "\1\u0985\106\0\2\u0986\55\0\1\u0987\40\0\1\u0988\56\0"+ + "\1\u0799\112\0\1\u0989\57\0\2\u098a\53\0\1\u098b\57\0"+ + "\1\u06b9\61\0\1\u098c\101\0\1\u098d\55\0\1\u01da\56\0"+ + "\1\u098e\33\0\1\u098f\76\0\1\u0990\72\0\1\u0991\50\0"+ + "\1\u0992\72\0\1\u0993\33\0\1\u0791\115\0\1\u0994\25\0"+ + "\1\u0995\60\0\1\u0996\57\0\1\u0997\110\0\2\u055b\60\0"+ + "\2\u0998\34\0\1\u0999\117\0\1\u099a\24\0\1\u099b\42\0"+ + "\2\7\11\0\1\7\1\0\4\7\1\u099c\1\7\1\0"+ + "\1\7\6\0\3\7\1\0\1\7\1\0\1\7\1\0"+ + "\15\7\2\0\2\7\11\0\1\7\1\0\3\7\1\u099d"+ + "\2\7\1\0\1\7\6\0\3\7\1\0\1\7\1\0"+ + "\1\7\1\0\15\7\2\0\2\7\11\0\1\7\1\0"+ + "\6\7\1\0\1\7\6\0\3\7\1\0\1\7\1\0"+ + "\1\7\1\0\4\7\1\u065c\10\7\2\0\2\7\11\0"+ + "\1\7\1\0\1\u099e\5\7\1\0\1\7\6\0\3\7"+ + "\1\0\1\7\1\0\1\7\1\0\15\7\2\0\2\7"+ + "\11\0\1\7\1\0\6\7\1\0\1\7\6\0\3\7"+ + "\1\0\1\7\1\0\1\7\1\u0970\1\u099f\14\7\2\0"+ + "\2\7\11\0\1\7\1\0\4\7\1\u0854\1\7\1\0"+ + "\1\7\6\0\3\7\1\0\1\7\1\0\1\7\1\0"+ + "\15\7\2\0\2\7\11\0\1\7\1\0\1\7\1\u09a0"+ + "\4\7\1\0\1\7\6\0\3\7\1\0\1\7\1\0"+ + "\1\7\1\0\7\7\1\u09a1\5\7\2\0\2\7\11\0"+ + "\1\7\1\0\1\u09a2\5\7\1\0\1\7\6\0\3\7"+ + "\1\0\1\7\1\0\1\7\1\0\15\7\2\0\2\7"+ + "\11\0\1\7\1\0\6\7\1\u0974\1\u09a3\6\0\3\7"+ + "\1\0\1\7\1\0\1\7\1\0\15\7\2\0\2\7"+ + "\11\0\1\7\1\0\6\7\1\0\1\7\6\0\3\7"+ + "\1\0\1\7\1\0\1\7\1\0\1\7\1\u09a4\13\7"+ + "\2\0\2\7\11\0\1\7\1\0\6\7\1\0\1\7"+ + "\6\0\3\7\1\0\1\7\1\0\1\7\1\0\10\7"+ + "\1\u09a5\4\7\2\0\2\7\11\0\1\7\1\0\6\7"+ + "\1\0\1\7\6\0\3\7\1\0\1\7\1\0\1\7"+ + "\1\0\4\7\1\u09a6\10\7\2\0\2\7\11\0\1\7"+ + "\1\0\6\7\1\0\1\7\6\0\2\7\1\u09a7\1\0"+ + "\1\7\1\0\1\7\1\0\15\7\2\0\2\7\11\0"+ + "\1\u09a8\1\0\6\7\1\0\1\7\6\0\3\7\1\0"+ + "\1\7\1\0\1\7\1\0\15\7\2\0\2\7\11\0"+ + "\1\7\1\0\6\7\1\0\1\7\6\0\1\u09a9\2\7"+ + "\1\0\1\7\1\0\1\7\1\0\15\7\2\0\2\7"+ + "\11\0\1\7\1\0\6\7\1\0\1\7\6\0\3\7"+ + "\1\0\1\7\1\0\1\7\1\0\12\7\1\326\2\7"+ + "\2\0\2\7\11\0\1\7\1\0\4\7\1\u09aa\1\7"+ + "\1\0\1\7\6\0\3\7\1\0\1\7\1\0\1\7"+ + "\1\0\15\7\2\0\2\7\11\0\1\7\1\0\1\u09ab"+ + "\5\7\1\0\1\7\6\0\3\7\1\0\1\7\1\0"+ + "\1\7\1\0\15\7\2\0\2\7\11\0\1\7\1\0"+ + "\6\7\1\0\1\7\6\0\3\7\1\0\1\7\1\0"+ + "\1\7\1\0\2\7\1\u09ac\12\7\2\0\2\7\11\0"+ + "\1\7\1\0\6\7\1\0\1\7\6\0\3\7\1\0"+ + "\1\u09ad\1\0\1\7\1\0\15\7\2\0\2\7\11\0"+ + "\1\7\1\0\6\7\1\u075b\1\u03de\6\0\3\7\1\0"+ + "\1\7\1\0\1\7\1\0\15\7\2\0\2\7\11\0"+ + "\1\7\1\0\1\u09ae\5\7\1\0\1\7\6\0\3\7"+ + "\1\0\1\7\1\0\1\7\1\0\15\7\2\0\2\7"+ + "\11\0\1\7\1\0\1\u09af\5\7\1\0\1\7\6\0"+ + "\3\7\1\0\1\7\1\0\1\7\1\0\15\7\2\0"+ + "\2\7\11\0\1\u09b0\1\0\6\7\1\0\1\7\6\0"+ + "\3\7\1\0\1\7\1\0\1\7\1\0\15\7\2\0"+ + "\2\7\11\0\1\7\1\0\1\u09b1\5\7\1\0\1\7"+ + "\6\0\3\7\1\0\1\7\1\0\1\7\1\0\15\7"+ + "\2\0\2\7\11\0\1\7\1\0\4\7\1\u05be\1\7"+ + "\1\0\1\7\6\0\3\7\1\0\1\7\1\0\1\7"+ + "\1\0\15\7\2\0\2\7\11\0\1\7\1\0\6\7"+ + "\1\0\1\7\6\0\3\7\1\0\1\7\1\0\1\7"+ + "\1\0\1\7\1\u09b2\13\7\2\0\2\7\11\0\1\7"+ + "\1\0\6\7\1\u0984\1\u09b3\6\0\3\7\1\0\1\7"+ + "\1\0\1\7\1\0\15\7\2\0\2\7\11\0\1\7"+ + "\1\0\1\u09b4\5\7\1\0\1\7\6\0\3\7\1\0"+ + "\1\7\1\0\1\7\1\0\15\7\2\0\2\7\11\0"+ + "\1\7\1\0\6\7\1\0\1\7\6\0\3\7\1\0"+ + "\1\7\1\0\1\7\1\u0986\1\u09b5\14\7\2\0\2\7"+ + "\11\0\1\7\1\0\6\7\1\0\1\7\6\0\3\7"+ + "\1\0\1\u09b6\1\0\1\7\1\0\15\7\2\0\2\7"+ + "\11\0\1\7\1\0\1\7\1\u09b7\4\7\1\0\1\7"+ + "\6\0\3\7\1\0\1\7\1\0\1\7\1\0\15\7"+ + "\2\0\2\7\11\0\1\u07eb\1\0\6\7\1\0\1\7"+ + "\6\0\3\7\1\0\1\7\1\0\1\7\1\0\15\7"+ + "\2\0\2\7\11\0\1\7\1\0\6\7\1\0\1\7"+ + "\6\0\3\7\1\0\1\7\1\0\1\7\1\u098a\1\u09b8"+ + "\14\7\2\0\2\7\11\0\1\7\1\0\6\7\1\0"+ + "\1\7\6\0\2\7\1\u09b9\1\0\1\7\1\0\1\7"+ + "\1\0\15\7\2\0\2\7\11\0\1\7\1\0\6\7"+ + "\1\0\1\7\6\0\1\u0713\2\7\1\0\1\7\1\0"+ + "\1\7\1\0\15\7\2\0\2\7\11\0\1\7\1\0"+ + "\6\7\1\0\1\7\6\0\1\u09ba\2\7\1\0\1\7"+ + "\1\0\1\7\1\0\15\7\2\0\2\7\11\0\1\7"+ + "\1\0\6\7\1\0\1\7\6\0\3\7\1\0\1\7"+ + "\1\0\1\7\1\0\10\7\1\116\4\7\2\0\2\7"+ + "\11\0\1\7\1\0\6\7\1\0\1\7\6\0\3\7"+ + "\1\0\1\7\1\0\1\7\1\0\4\7\1\u011c\10\7"+ + "\2\0\2\7\11\0\1\7\1\0\6\7\1\0\1\7"+ + "\6\0\3\7\1\0\1\7\1\0\1\7\1\0\1\7"+ + "\1\u09bb\13\7\2\0\2\7\11\0\1\7\1\0\1\7"+ + "\1\u09bc\4\7\1\0\1\7\6\0\3\7\1\0\1\7"+ + "\1\0\1\7\1\0\15\7\2\0\2\7\11\0\1\7"+ + "\1\0\6\7\1\0\1\7\6\0\1\u09bd\2\7\1\0"+ + "\1\7\1\0\1\7\1\0\15\7\2\0\2\7\11\0"+ + "\1\7\1\0\6\7\1\0\1\7\6\0\3\7\1\0"+ + "\1\7\1\0\1\7\1\0\1\7\1\u09be\13\7\2\0"+ + "\2\7\11\0\1\7\1\0\6\7\1\0\1\7\6\0"+ + "\1\u09bf\2\7\1\0\1\7\1\0\1\7\1\0\15\7"+ + "\2\0\2\7\11\0\1\7\1\0\6\7\1\0\1\7"; + + private static final String ZZ_TRANS_PACKED_1 = + "\6\0\3\7\1\0\1\7\1\0\1\7\1\0\1\7"+ + "\1\u09c0\13\7\2\0\2\7\11\0\1\7\1\0\1\7"+ + "\1\u07e3\4\7\1\0\1\7\6\0\3\7\1\0\1\7"+ + "\1\0\1\7\1\0\15\7\2\0\2\7\11\0\1\7"+ + "\1\0\6\7\1\0\1\7\6\0\3\7\1\0\1\7"+ + "\1\0\1\7\1\0\7\7\1\u09c1\5\7\2\0\2\7"+ + "\11\0\1\7\1\0\1\7\1\u09c2\4\7\1\0\1\7"+ + "\6\0\3\7\1\0\1\7\1\0\1\7\1\0\15\7"+ + "\2\0\2\7\11\0\1\7\1\0\1\u09c3\5\7\1\0"+ + "\1\7\6\0\3\7\1\0\1\7\1\0\1\7\1\0"+ + "\15\7\2\0\2\7\11\0\1\u09c4\1\0\6\7\1\0"+ + "\1\7\6\0\3\7\1\0\1\7\1\0\1\7\1\0"+ + "\15\7\2\0\2\7\11\0\1\7\1\0\6\7\1\0"+ + "\1\7\6\0\3\7\1\0\1\7\1\0\1\7\1\u055b"+ + "\1\u05be\14\7\2\0\2\7\11\0\1\7\1\0\6\7"+ + "\1\0\1\7\6\0\3\7\1\0\1\7\1\0\1\7"+ + "\1\u0998\1\u09c5\14\7\2\0\2\7\11\0\1\7\1\0"+ + "\1\7\1\u09c6\4\7\1\0\1\7\6\0\3\7\1\0"+ + "\1\7\1\0\1\7\1\0\15\7\2\0\2\7\11\0"+ + "\1\7\1\0\6\7\1\0\1\7\6\0\3\7\1\0"+ + "\1\7\1\0\1\7\1\0\11\7\1\u09c7\3\7\2\0"+ + "\2\7\11\0\1\7\1\0\2\7\1\u09c8\3\7\1\0"+ + "\1\7\6\0\3\7\1\0\1\7\1\0\1\7\1\0"+ + "\15\7\55\0\1\u09c9\43\0\1\u09ca\42\0\1\u0526\111\0"+ + "\1\u09cb\26\0\1\u09cc\67\0\1\u09cd\75\0\1\u09ce\45\0"+ + "\1\u09cf\55\0\1\u09d0\77\0\1\u0810\43\0\1\u076c\21\0"+ + "\1\u09d1\40\0\1\u0799\76\0\1\u09d2\74\0\1\u09d3\66\0"+ + "\1\u09d4\52\0\1\u09d5\34\0\1\u0550\104\0\2\u09d6\35\0"+ + "\1\u0209\57\0\1\u09d7\65\0\1\u09d8\56\0\1\u09d9\65\0"+ + "\1\u09da\52\0\1\u09db\110\0\2\u09dc\33\0\1\u0815\34\0"+ + "\1\u09dd\31\0\1\u0918\112\0\1\u09de\46\0\2\u024e\53\0"+ + "\1\u09df\61\0\1\u09e0\43\0\1\u0881\112\0\1\u0237\62\0"+ + "\1\u09e1\54\0\2\u0145\33\0\1\u09e2\61\0\1\u09e3\57\0"+ + "\1\u09e4\64\0\1\u09e5\77\0\1\u09e6\40\0\1\u09e7\103\0"+ + "\1\u09e8\42\0\1\u0776\110\0\1\u09e9\50\0\1\u09ea\40\0"+ + "\1\u09eb\64\0\1\u09ec\43\0\2\7\11\0\1\7\1\0"+ + "\6\7\1\0\1\7\6\0\3\7\1\0\1\7\1\0"+ + "\1\7\1\0\10\7\1\u09ed\4\7\2\0\2\7\11\0"+ + "\1\7\1\0\6\7\1\0\1\7\6\0\2\7\1\u09ee"+ + "\1\0\1\7\1\0\1\7\1\0\15\7\2\0\2\7"+ + "\11\0\1\7\1\0\1\7\1\u0589\4\7\1\0\1\7"+ + "\6\0\3\7\1\0\1\7\1\0\1\7\1\0\15\7"+ + "\2\0\2\7\11\0\1\7\1\0\6\7\1\0\1\7"+ + "\6\0\3\7\1\0\1\7\1\0\1\7\1\0\3\7"+ + "\1\u09ef\11\7\2\0\2\7\11\0\1\u09f0\1\0\6\7"+ + "\1\0\1\7\6\0\3\7\1\0\1\7\1\0\1\7"+ + "\1\0\15\7\2\0\2\7\11\0\1\7\1\0\4\7"+ + "\1\u09f1\1\7\1\0\1\7\6\0\3\7\1\0\1\7"+ + "\1\0\1\7\1\0\15\7\2\0\2\7\11\0\1\7"+ + "\1\0\6\7\1\0\1\7\6\0\2\7\1\u09f2\1\0"+ + "\1\7\1\0\1\7\1\0\15\7\2\0\2\7\11\0"+ + "\1\7\1\0\4\7\1\u09f3\1\7\1\0\1\7\6\0"+ + "\3\7\1\0\1\7\1\0\1\7\1\0\15\7\2\0"+ + "\2\7\11\0\1\7\1\0\1\u09f4\5\7\1\0\1\7"+ + "\6\0\3\7\1\0\1\7\1\0\1\7\1\0\15\7"+ + "\2\0\2\7\11\0\1\7\1\0\6\7\1\0\1\7"+ + "\6\0\1\u0855\2\7\1\0\1\7\1\0\1\7\1\0"+ + "\15\7\2\0\2\7\11\0\1\7\1\0\1\u07be\5\7"+ + "\1\0\1\7\6\0\3\7\1\0\1\u09f5\1\0\1\7"+ + "\1\0\15\7\2\0\2\7\11\0\1\7\1\0\1\7"+ + "\1\u07eb\4\7\1\0\1\7\6\0\3\7\1\0\1\7"+ + "\1\0\1\7\1\0\15\7\2\0\2\7\11\0\1\7"+ + "\1\0\6\7\1\0\1\7\6\0\1\u09f6\2\7\1\0"+ + "\1\7\1\0\1\7\1\0\15\7\2\0\2\7\11\0"+ + "\1\7\1\0\6\7\1\0\1\7\6\0\3\7\1\0"+ + "\1\7\1\0\1\7\1\0\3\7\1\u09f7\11\7\2\0"+ + "\2\7\11\0\1\7\1\0\6\7\1\0\1\7\6\0"+ + "\3\7\1\0\1\7\1\0\1\7\1\0\10\7\1\u09f8"+ + "\4\7\2\0\2\7\11\0\1\7\1\0\6\7\1\0"+ + "\1\7\6\0\3\7\1\0\1\7\1\0\1\7\1\0"+ + "\1\7\1\u09f9\13\7\2\0\2\7\11\0\1\7\1\0"+ + "\2\7\1\u05b3\3\7\1\0\1\7\6\0\3\7\1\0"+ + "\1\7\1\0\1\7\1\0\15\7\2\0\2\7\11\0"+ + "\1\7\1\0\6\7\1\0\1\7\6\0\3\7\1\0"+ + "\1\7\1\0\1\7\1\u09d6\1\u09fa\14\7\2\0\2\7"+ + "\11\0\1\7\1\0\2\7\1\u020a\3\7\1\0\1\7"+ + "\6\0\3\7\1\0\1\7\1\0\1\7\1\0\15\7"+ + "\2\0\2\7\11\0\1\7\1\0\1\u09fb\5\7\1\0"+ + "\1\7\6\0\3\7\1\0\1\7\1\0\1\7\1\0"+ + "\15\7\2\0\2\7\11\0\1\7\1\0\4\7\1\u09fc"+ + "\1\7\1\0\1\7\6\0\3\7\1\0\1\7\1\0"+ + "\1\7\1\0\15\7\2\0\2\7\11\0\1\7\1\0"+ + "\1\7\1\u09fd\4\7\1\0\1\7\6\0\3\7\1\0"+ + "\1\7\1\0\1\7\1\0\15\7\2\0\2\7\11\0"+ + "\1\7\1\0\5\7\1\u09fe\1\0\1\7\6\0\3\7"+ + "\1\0\1\7\1\0\1\7\1\0\15\7\2\0\2\7"+ + "\11\0\1\u09ff\1\0\6\7\1\0\1\7\6\0\3\7"+ + "\1\0\1\7\1\0\1\7\1\0\15\7\2\0\2\7"+ + "\11\0\1\7\1\0\6\7\1\0\1\7\6\0\3\7"+ + "\1\0\1\7\1\0\1\7\1\u09dc\1\u0a00\14\7\2\0"+ + "\2\7\11\0\1\7\1\0\1\u085a\5\7\1\0\1\7"+ + "\6\0\3\7\1\0\1\7\1\0\1\7\1\0\7\7"+ + "\1\u0a01\5\7\2\0\2\7\11\0\1\7\1\0\5\7"+ + "\1\u0950\1\0\1\7\6\0\3\7\1\0\1\7\1\0"+ + "\1\7\1\0\15\7\2\0\2\7\11\0\1\7\1\0"+ + "\6\7\1\0\1\7\6\0\3\7\1\0\1\7\1\0"+ + "\1\7\1\0\10\7\1\u0a02\4\7\2\0\2\7\11\0"+ + "\1\7\1\0\6\7\1\0\1\7\6\0\1\u0a03\2\7"+ + "\1\0\1\7\1\0\1\7\1\0\15\7\2\0\2\7"+ + "\11\0\1\7\1\0\6\7\1\0\1\7\6\0\1\u0a04"+ + "\2\7\1\0\1\7\1\0\1\7\1\0\15\7\2\0"+ + "\2\7\11\0\1\7\1\0\1\u026b\5\7\1\0\1\7"+ + "\6\0\3\7\1\0\1\7\1\0\1\7\1\0\15\7"+ + "\2\0\2\7\11\0\1\7\1\0\6\7\1\0\1\7"+ + "\6\0\3\7\1\0\1\7\1\0\1\7\1\0\4\7"+ + "\1\u0a05\10\7\2\0\2\7\11\0\1\7\1\0\6\7"+ + "\1\0\1\7\6\0\3\7\1\0\1\7\1\0\1\7"+ + "\1\u0145\1\u0146\14\7\2\0\2\7\11\0\1\7\1\0"+ + "\1\u0a06\5\7\1\0\1\7\6\0\3\7\1\0\1\7"+ + "\1\0\1\7\1\0\15\7\2\0\2\7\11\0\1\7"+ + "\1\0\1\u0a07\5\7\1\0\1\7\6\0\3\7\1\0"+ + "\1\7\1\0\1\7\1\0\15\7\2\0\2\7\11\0"+ + "\1\u0a08\1\0\6\7\1\0\1\7\6\0\3\7\1\0"+ + "\1\7\1\0\1\7\1\0\15\7\2\0\2\7\11\0"+ + "\1\7\1\0\1\7\1\u0a09\4\7\1\0\1\7\6\0"+ + "\3\7\1\0\1\7\1\0\1\7\1\0\15\7\2\0"+ + "\2\7\11\0\1\7\1\0\6\7\1\0\1\7\6\0"+ + "\1\7\1\u0a0a\1\7\1\0\1\7\1\0\1\7\1\0"+ + "\15\7\2\0\2\7\11\0\1\u0a0b\1\0\6\7\1\0"+ + "\1\7\6\0\3\7\1\0\1\7\1\0\1\7\1\0"+ + "\15\7\2\0\2\7\11\0\1\7\1\0\6\7\1\0"+ + "\1\7\6\0\2\7\1\u0a0c\1\0\1\7\1\0\1\7"+ + "\1\0\15\7\2\0\2\7\11\0\1\7\1\0\1\7"+ + "\1\u07c8\4\7\1\0\1\7\6\0\3\7\1\0\1\7"+ + "\1\0\1\7\1\0\15\7\2\0\2\7\11\0\1\7"+ + "\1\0\6\7\1\0\1\7\6\0\3\7\1\0\1\7"+ + "\1\0\1\7\1\0\2\7\1\u0a0d\12\7\2\0\2\7"+ + "\11\0\1\7\1\0\6\7\1\0\1\7\6\0\1\7"+ + "\1\u0a0e\1\7\1\0\1\7\1\0\1\7\1\0\15\7"+ + "\2\0\2\7\11\0\1\u0a0f\1\0\6\7\1\0\1\7"+ + "\6\0\3\7\1\0\1\7\1\0\1\7\1\0\15\7"+ + "\2\0\2\7\11\0\1\7\1\0\1\7\1\u0a10\4\7"+ + "\1\0\1\7\6\0\3\7\1\0\1\7\1\0\1\7"+ + "\1\0\15\7\15\0\1\u0a11\112\0\1\u0a12\37\0\1\u0a13"+ + "\115\0\1\u0a14\45\0\2\u0a15\34\0\1\u0a16\117\0\1\u0a17"+ + "\20\0\1\u0a18\67\0\1\u0780\55\0\1\u0a19\63\0\1\u01da"+ + "\65\0\2\u0a1a\72\0\1\u0a1b\46\0\1\u0a1c\74\0\1\u0a1d"+ + "\42\0\1\u0a1e\64\0\1\u0694\102\0\2\u091f\40\0\1\372"+ + "\57\0\1\u0526\107\0\1\u0a1f\57\0\1\u0a20\65\0\1\u0a21"+ + "\53\0\2\u0a22\37\0\1\u0a23\102\0\2\u0a24\31\0\1\u0149"+ + "\120\0\1\u0a25\22\0\1\u0a26\61\0\1\u0a27\124\0\1\u0a28"+ + "\40\0\1\u0a29\57\0\1\u0a2a\65\0\1\u0a2b\40\0\1\u0a2c"+ + "\76\0\1\u0a2d\26\0\2\7\11\0\1\u0a2e\1\0\6\7"+ + "\1\0\1\7\6\0\3\7\1\0\1\7\1\0\1\7"+ + "\1\0\15\7\2\0\2\7\11\0\1\7\1\0\6\7"+ + "\1\0\1\7\6\0\3\7\1\0\1\7\1\0\1\7"+ + "\1\0\1\7\1\u0a2f\13\7\2\0\2\7\11\0\1\7"+ + "\1\0\5\7\1\u0a30\1\0\1\7\6\0\3\7\1\0"+ + "\1\7\1\0\1\7\1\0\15\7\2\0\2\7\11\0"+ + "\1\7\1\0\6\7\1\0\1\7\6\0\3\7\1\0"+ + "\1\7\1\0\1\7\1\0\13\7\1\u0a31\1\7\2\0"+ + "\2\7\11\0\1\7\1\0\6\7\1\0\1\7\6\0"+ + "\3\7\1\0\1\7\1\0\1\7\1\u0a15\1\u0a32\14\7"+ + "\2\0\2\7\11\0\1\7\1\0\1\7\1\u0a33\4\7"+ + "\1\0\1\7\6\0\3\7\1\0\1\7\1\0\1\7"+ + "\1\0\15\7\2\0\2\7\11\0\1\7\1\0\6\7"+ + "\1\0\1\7\6\0\3\7\1\0\1\7\1\0\1\7"+ + "\1\0\11\7\1\u0a34\3\7\2\0\2\7\11\0\1\u0a35"+ + "\1\0\6\7\1\0\1\7\6\0\3\7\1\0\1\7"+ + "\1\0\1\7\1\0\15\7\2\0\2\7\11\0\1\7"+ + "\1\0\4\7\1\u07d2\1\7\1\0\1\7\6\0\3\7"+ + "\1\0\1\7\1\0\1\7\1\0\15\7\2\0\2\7"+ + "\11\0\1\7\1\0\1\u0a36\5\7\1\0\1\7\6\0"+ + "\3\7\1\0\1\7\1\0\1\7\1\0\15\7\2\0"+ + "\2\7\11\0\1\7\1\0\2\7\1\u011c\3\7\1\0"+ + "\1\7\6\0\3\7\1\0\1\7\1\0\1\7\1\0"+ + "\15\7\2\0\2\7\11\0\1\7\1\0\6\7\1\u0a1a"+ + "\1\u0a37\6\0\3\7\1\0\1\7\1\0\1\7\1\0"+ + "\15\7\2\0\2\7\11\0\1\7\1\0\6\7\1\0"+ + "\1\7\6\0\2\7\1\u0a38\1\0\1\7\1\0\1\7"+ + "\1\0\15\7\2\0\2\7\11\0\1\7\1\0\5\7"+ + "\1\u0a39\1\0\1\7\6\0\3\7\1\0\1\7\1\0"+ + "\1\7\1\0\15\7\2\0\2\7\11\0\1\7\1\0"+ + "\6\7\1\0\1\7\6\0\2\7\1\u0a3a\1\0\1\7"+ + "\1\0\1\7\1\0\15\7\2\0\2\7\11\0\1\7"+ + "\1\0\1\7\1\u0a3b\4\7\1\0\1\7\6\0\3\7"+ + "\1\0\1\7\1\0\1\7\1\0\15\7\2\0\2\7"+ + "\11\0\1\7\1\0\4\7\1\u06ef\1\7\1\0\1\7"+ + "\6\0\3\7\1\0\1\7\1\0\1\7\1\0\15\7"+ + "\2\0\2\7\11\0\1\7\1\0\6\7\1\0\1\7"+ + "\6\0\3\7\1\0\1\7\1\0\1\7\1\u091f\1\u018d"+ + "\14\7\2\0\2\7\11\0\1\7\1\0\5\7\1\u010a"+ + "\1\0\1\7\6\0\3\7\1\0\1\7\1\0\1\7"+ + "\1\0\15\7\2\0\2\7\11\0\1\7\1\0\3\7"+ + "\1\u0589\2\7\1\0\1\7\6\0\3\7\1\0\1\7"+ + "\1\0\1\7\1\0\15\7\2\0\2\7\11\0\1\7"+ + "\1\0\6\7\1\0\1\7\6\0\3\7\1\0\1\7"+ + "\1\0\1\7\1\0\3\7\1\u0a3c\11\7\2\0\2\7"+ + "\11\0\1\7\1\0\6\7\1\0\1\7\6\0\3\7"+ + "\1\0\1\7\1\0\1\7\1\0\1\7\1\u0a3d\13\7"+ + "\2\0\2\7\11\0\1\7\1\0\6\7\1\0\1\7"+ + "\6\0\3\7\1\0\1\7\1\0\1\7\1\0\5\7"+ + "\1\u0a3e\7\7\2\0\2\7\11\0\1\7\1\0\6\7"+ + "\1\0\1\7\6\0\3\7\1\0\1\7\1\0\1\7"+ + "\1\u0a22\1\u0a3f\14\7\2\0\2\7\11\0\1\7\1\0"+ + "\4\7\1\u0a40\1\7\1\0\1\7\6\0\3\7\1\0"+ + "\1\7\1\0\1\7\1\0\15\7\2\0\2\7\11\0"+ + "\1\7\1\0\6\7\1\0\1\7\6\0\3\7\1\0"+ + "\1\7\1\0\1\7\1\u0a24\1\u0a41\14\7\2\0\2\7"+ + "\11\0\1\u014a\1\0\6\7\1\0\1\7\6\0\3\7"+ + "\1\0\1\7\1\0\1\7\1\0\15\7\2\0\2\7"+ + "\11\0\1\7\1\0\6\7\1\0\1\7\6\0\3\7"+ + "\1\0\1\7\1\0\1\7\1\0\7\7\1\u0a42\5\7"+ + "\2\0\2\7\11\0\1\u0a43\1\0\6\7\1\0\1\7"+ + "\6\0\3\7\1\0\1\7\1\0\1\7\1\0\15\7"+ + "\2\0\2\7\11\0\1\u0a44\1\0\6\7\1\0\1\7"+ + "\6\0\3\7\1\0\1\7\1\0\1\7\1\0\15\7"+ + "\2\0\2\7\11\0\1\7\1\0\6\7\1\0\1\7"+ + "\6\0\3\7\1\0\1\7\1\0\1\7\1\0\13\7"+ + "\1\u0a45\1\7\2\0\2\7\11\0\1\7\1\0\6\7"+ + "\1\0\1\7\6\0\2\7\1\u0a46\1\0\1\7\1\0"+ + "\1\7\1\0\15\7\2\0\2\7\11\0\1\7\1\0"+ + "\6\7\1\0\1\7\6\0\1\u0a47\2\7\1\0\1\7"+ + "\1\0\1\7\1\0\15\7\2\0\2\7\11\0\1\7"+ + "\1\0\6\7\1\0\1\7\6\0\3\7\1\0\1\u0a48"+ + "\1\0\1\7\1\0\15\7\2\0\2\7\11\0\1\7"+ + "\1\0\1\7\1\u0a49\4\7\1\0\1\7\6\0\3\7"+ + "\1\0\1\7\1\0\1\7\1\0\15\7\2\0\2\7"+ + "\11\0\1\7\1\0\6\7\1\0\1\7\6\0\1\u0a4a"+ + "\2\7\1\0\1\7\1\0\1\7\1\0\15\7\20\0"+ + "\1\u076e\62\0\1\u01dc\63\0\1\u0a4b\57\0\1\u0a4c\75\0"+ + "\1\u0a4d\101\0\1\u0a4e\21\0\1\u0a4f\64\0\1\u0a50\100\0"+ + "\1\u096f\66\0\2\u0a51\40\0\1\u0a52\101\0\2\u0a53\51\0"+ + "\1\u0609\47\0\1\u0a54\75\0\1\u0a55\61\0\1\u0a56\61\0"+ + "\1\u0a57\57\0\1\u0a58\44\0\1\u0a59\63\0\1\u0a5a\76\0"+ + "\1\u0a5b\37\0\1\u0a5c\71\0\2\u0a5d\54\0\1\u0a5e\64\0"+ + "\1\u0a5f\52\0\1\u0a60\65\0\1\u0a61\77\0\1\u0a62\45\0"+ + "\1\u0a63\40\0\2\7\11\0\1\7\1\0\1\7\1\u07c0"+ + "\4\7\1\0\1\7\6\0\3\7\1\0\1\7\1\0"+ + "\1\7\1\0\15\7\2\0\2\7\11\0\1\7\1\0"+ + "\2\7\1\u01dd\3\7\1\0\1\7\6\0\3\7\1\0"+ + "\1\7\1\0\1\7\1\0\15\7\2\0\2\7\11\0"+ + "\1\7\1\0\4\7\1\u0a64\1\7\1\0\1\7\6\0"+ + "\3\7\1\0\1\7\1\0\1\7\1\0\15\7\2\0"+ + "\2\7\11\0\1\7\1\0\2\7\1\u0a65\3\7\1\0"+ + "\1\7\6\0\3\7\1\0\1\7\1\0\1\7\1\0"+ + "\15\7\2\0\2\7\11\0\1\7\1\0\6\7\1\0"+ + "\1\7\6\0\1\u0a66\2\7\1\0\1\7\1\0\1\7"+ + "\1\0\15\7\2\0\2\7\11\0\1\7\1\0\6\7"+ + "\1\0\1\7\6\0\3\7\1\0\1\7\1\0\1\7"+ + "\1\0\10\7\1\u0a67\4\7\2\0\2\7\11\0\1\u0a68"+ + "\1\0\6\7\1\0\1\7\6\0\3\7\1\0\1\7"+ + "\1\0\1\7\1\0\15\7\2\0\2\7\11\0\1\7"+ + "\1\0\1\7\1\u0a69\4\7\1\0\1\7\6\0\3\7"+ + "\1\0\1\7\1\0\1\7\1\0\15\7\2\0\2\7"+ + "\11\0\1\7\1\0\6\7\1\0\1\7\6\0\2\7"+ + "\1\u099e\1\0\1\7\1\0\1\7\1\0\15\7\2\0"+ + "\2\7\11\0\1\7\1\0\6\7\1\0\1\7\6\0"+ + "\3\7\1\0\1\7\1\0\1\7\1\u0a51\1\u0a6a\14\7"+ + "\2\0\2\7\11\0\1\7\1\0\5\7\1\u0a6b\1\0"+ + "\1\7\6\0\3\7\1\0\1\7\1\0\1\7\1\0"+ + "\15\7\2\0\2\7\11\0\1\7\1\0\6\7\1\0"+ + "\1\7\6\0\3\7\1\0\1\7\1\0\1\7\1\u0a53"+ + "\1\u0a6c\14\7\2\0\2\7\11\0\1\7\1\0\6\7"+ + "\1\0\1\7\6\0\1\u066a\2\7\1\0\1\7\1\0"+ + "\1\7\1\0\15\7\2\0\2\7\11\0\1\7\1\0"+ + "\4\7\1\u0a6d\1\7\1\0\1\7\6\0\3\7\1\0"+ + "\1\7\1\0\1\7\1\0\15\7\2\0\2\7\11\0"+ + "\1\7\1\0\6\7\1\0\1\7\6\0\2\7\1\u0a6e"+ + "\1\0\1\7\1\0\1\7\1\0\15\7\2\0\2\7"+ + "\11\0\1\7\1\0\6\7\1\0\1\7\6\0\2\7"+ + "\1\u0a6f\1\0\1\7\1\0\1\7\1\0\15\7\2\0"+ + "\2\7\11\0\1\7\1\0\6\7\1\0\1\7\6\0"+ + "\2\7\1\u0a70\1\0\1\7\1\0\1\7\1\0\15\7"+ + "\2\0\2\7\11\0\1\7\1\0\6\7\1\0\1\7"+ + "\6\0\1\u0a71\2\7\1\0\1\7\1\0\1\7\1\0"+ + "\15\7\2\0\2\7\11\0\1\7\1\0\1\7\1\u0a72"+ + "\4\7\1\0\1\7\6\0\3\7\1\0\1\7\1\0"+ + "\1\7\1\0\15\7\2\0\2\7\11\0\1\7\1\0"+ + "\3\7\1\u0a73\2\7\1\0\1\7\6\0\3\7\1\0"+ + "\1\7\1\0\1\7\1\0\15\7\2\0\2\7\11\0"+ + "\1\7\1\0\6\7\1\0\1\7\6\0\2\7\1\u0a74"+ + "\1\0\1\7\1\0\1\7\1\0\15\7\2\0\2\7"+ + "\11\0\1\u0a75\1\0\6\7\1\0\1\7\6\0\3\7"+ + "\1\0\1\7\1\0\1\7\1\0\15\7\2\0\2\7"+ + "\11\0\1\7\1\0\6\7\1\u0a5d\1\u0a76\6\0\3\7"+ + "\1\0\1\7\1\0\1\7\1\0\15\7\2\0\2\7"+ + "\11\0\1\7\1\0\2\7\1\u0a77\3\7\1\0\1\7"+ + "\6\0\3\7\1\0\1\7\1\0\1\7\1\0\15\7"+ + "\2\0\2\7\11\0\1\7\1\0\5\7\1\u0a78\1\0"+ + "\1\7\6\0\3\7\1\0\1\7\1\0\1\7\1\0"+ + "\15\7\2\0\2\7\11\0\1\u0a79\1\0\6\7\1\0"+ + "\1\7\6\0\3\7\1\0\1\7\1\0\1\7\1\0"+ + "\15\7\2\0\2\7\11\0\1\7\1\0\2\7\1\u0a7a"+ + "\3\7\1\0\1\7\6\0\3\7\1\0\1\7\1\0"+ + "\1\7\1\0\15\7\2\0\2\7\11\0\1\7\1\0"+ + "\6\7\1\0\1\7\6\0\2\7\1\u0a7b\1\0\1\7"+ + "\1\0\1\7\1\0\15\7\2\0\2\7\11\0\1\7"+ + "\1\0\4\7\1\u0a7c\1\7\1\0\1\7\6\0\3\7"+ + "\1\0\1\7\1\0\1\7\1\0\15\7\55\0\1\u0a7d"+ + "\21\0\1\u0a7e\114\0\1\u0a7f\31\0\1\u0971\66\0\2\u0a80"+ + "\53\0\1\u0a81\110\0\1\u0a82\51\0\1\u0149\41\0\1\u0526"+ + "\77\0\1\u0a83\44\0\1\u06c2\76\0\1\u0a84\44\0\1\u0a85"+ + "\111\0\1\u0a86\34\0\1\u0a87\113\0\1\u0609\42\0\1\u0237"+ + "\60\0\1\u0976\72\0\1\u0a88\57\0\2\u0a89\33\0\1\u0a8a"+ + "\100\0\1\u0a8b\42\0\1\u0a8c\103\0\1\u0a8d\44\0\1\u09ea"+ + "\37\0\2\7\11\0\1\7\1\0\6\7\1\0\1\7"+ + "\6\0\3\7\1\0\1\7\1\0\1\7\1\0\10\7"+ + "\1\u0a8e\4\7\2\0\2\7\11\0\1\u0a8f\1\0\6\7"+ + "\1\0\1\7\6\0\3\7\1\0\1\7\1\0\1\7"+ + "\1\0\15\7\2\0\2\7\11\0\1\7\1\0\6\7"+ + "\1\0\1\7\6\0\3\7\1\0\1\7\1\0\1\7"+ + "\1\0\3\7\1\u0a90\11\7\2\0\2\7\11\0\1\7"+ + "\1\0\1\7\1\u09a0\4\7\1\0\1\7\6\0\3\7"+ + "\1\0\1\7\1\0\1\7\1\0\15\7\2\0\2\7"+ + "\11\0\1\7\1\0\6\7\1\u0a80\1\u0a91\6\0\3\7"+ + "\1\0\1\7\1\0\1\7\1\0\15\7\2\0\2\7"+ + "\11\0\1\7\1\0\1\7\1\u0a92\4\7\1\0\1\7"+ + "\6\0\3\7\1\0\1\7\1\0\1\7\1\0\15\7"+ + "\2\0\2\7\11\0\1\7\1\0\6\7\1\0\1\7"+ + "\6\0\3\7\1\0\1\7\1\0\1\7\1\0\2\7"+ + "\1\u0a93\12\7\2\0\2\7\11\0\1\7\1\0\6\7"+ + "\1\0\1\7\6\0\2\7\1\u014a\1\0\1\7\1\0"+ + "\1\7\1\0\15\7\2\0\2\7\11\0\1\7\1\0"+ + "\1\u0589\5\7\1\0\1\7\6\0\3\7\1\0\1\7"+ + "\1\0\1\7\1\0\15\7\2\0\2\7\11\0\1\7"+ + "\1\0\6\7\1\0\1\7\6\0\1\u0a94\2\7\1\0"+ + "\1\7\1\0\1\7\1\0\15\7\2\0\2\7\11\0"+ + "\1\7\1\0\1\7\1\u071c\4\7\1\0\1\7\6\0"+ + "\3\7\1\0\1\7\1\0\1\7\1\0\15\7\2\0"+ + "\2\7\11\0\1\7\1\0\6\7\1\0\1\7\6\0"+ + "\1\u0a95\2\7\1\0\1\7\1\0\1\7\1\0\15\7"+ + "\2\0\2\7\11\0\1\7\1\0\1\7\1\u0a96\4\7"+ + "\1\0\1\7\6\0\3\7\1\0\1\7\1\0\1\7"+ + "\1\0\15\7\2\0\2\7\11\0\1\7\1\0\6\7"+ + "\1\0\1\7\6\0\3\7\1\0\1\7\1\0\1\7"+ + "\1\0\3\7\1\u0a97\11\7\2\0\2\7\11\0\1\7"+ + "\1\0\4\7\1\u0a98\1\7\1\0\1\7\6\0\3\7"+ + "\1\0\1\7\1\0\1\7\1\0\15\7\2\0\2\7"+ + "\11\0\1\7\1\0\6\7\1\0\1\7\6\0\3\7"+ + "\1\0\1\7\1\0\1\7\1\0\10\7\1\u066a\4\7"+ + "\2\0\2\7\11\0\1\7\1\0\6\7\1\0\1\7"+ + "\6\0\1\7\1\326\1\7\1\0\1\7\1\0\1\7"+ + "\1\0\15\7\2\0\2\7\11\0\1\7\1\0\6\7"+ + "\1\0\1\7\6\0\1\u09a5\2\7\1\0\1\7\1\0"+ + "\1\7\1\0\15\7\2\0\2\7\11\0\1\7\1\0"+ + "\6\7\1\0\1\7\6\0\3\7\1\0\1\7\1\0"+ + "\1\7\1\0\1\7\1\u0a99\13\7\2\0\2\7\11\0"+ + "\1\7\1\0\6\7\1\0\1\7\6\0\3\7\1\0"+ + "\1\7\1\0\1\7\1\u0a89\1\u0a9a\14\7\2\0\2\7"+ + "\11\0\1\7\1\0\1\u0a9b\5\7\1\0\1\7\6\0"+ + "\3\7\1\0\1\7\1\0\1\7\1\0\15\7\2\0"+ + "\2\7\11\0\1\7\1\0\6\7\1\0\1\7\6\0"+ + "\1\7\1\u0a9c\1\7\1\0\1\7\1\0\1\7\1\0"+ + "\15\7\2\0\2\7\11\0\1\7\1\0\1\u0a9d\5\7"+ + "\1\0\1\7\6\0\3\7\1\0\1\7\1\0\1\7"+ + "\1\0\15\7\2\0\2\7\11\0\1\7\1\0\6\7"+ + "\1\0\1\7\6\0\3\7\1\0\1\u0a9e\1\0\1\7"+ + "\1\0\15\7\2\0\2\7\11\0\1\7\1\0\5\7"+ + "\1\u0a0e\1\0\1\7\6\0\3\7\1\0\1\7\1\0"+ + "\1\7\1\0\15\7\15\0\1\u0a9f\71\0\2\u0aa0\56\0"+ + "\1\u0aa1\55\0\1\u0aa2\57\0\1\u0aa3\101\0\1\u0aa4\72\0"+ + "\1\u0237\32\0\1\u0aa5\66\0\1\u0799\55\0\1\u0aa6\107\0"+ + "\1\u0aa7\33\0\1\u0aa8\61\0\1\u091e\105\0\2\u05df\60\0"+ + "\2\u02f5\33\0\1\u02ae\44\0\1\372\61\0\2\7\11\0"+ + "\1\u0aa9\1\0\6\7\1\0\1\7\6\0\3\7\1\0"+ + "\1\7\1\0\1\7\1\0\15\7\2\0\2\7\11\0"+ + "\1\7\1\0\6\7\1\u0aa0\1\u0aaa\6\0\3\7\1\0"+ + "\1\7\1\0\1\7\1\0\15\7\2\0\2\7\11\0"+ + "\1\7\1\0\4\7\1\u0aab\1\7\1\0\1\7\6\0"+ + "\3\7\1\0\1\7\1\0\1\7\1\0\15\7\2\0"+ + "\2\7\11\0\1\7\1\0\1\u0aac\5\7\1\0\1\7"+ + "\6\0\3\7\1\0\1\7\1\0\1\7\1\0\15\7"+ + "\2\0\2\7\11\0\1\u0aad\1\0\6\7\1\0\1\7"+ + "\6\0\3\7\1\0\1\7\1\0\1\7\1\0\15\7"+ + "\2\0\2\7\11\0\1\7\1\0\6\7\1\0\1\7"+ + "\6\0\1\u0aae\2\7\1\0\1\7\1\0\1\7\1\0"+ + "\15\7\2\0\2\7\11\0\1\7\1\0\6\7\1\0"+ + "\1\7\6\0\3\7\1\0\1\7\1\0\1\7\1\0"+ + "\1\7\1\326\13\7\2\0\2\7\11\0\1\7\1\0"+ + "\1\u0aaf\5\7\1\0\1\7\6\0\3\7\1\0\1\7"+ + "\1\0\1\7\1\0\15\7\2\0\2\7\11\0\1\7"+ + "\1\0\5\7\1\u07eb\1\0\1\7\6\0\3\7\1\0"+ + "\1\7\1\0\1\7\1\0\15\7\2\0\2\7\11\0"+ + "\1\7\1\0\1\7\1\u0ab0\4\7\1\0\1\7\6\0"+ + "\3\7\1\0\1\7\1\0\1\7\1\0\15\7\2\0"+ + "\2\7\11\0\1\7\1\0\6\7\1\0\1\7\6\0"+ + "\3\7\1\0\1\7\1\0\1\7\1\0\1\7\1\u0ab1"+ + "\13\7\2\0\2\7\11\0\1\7\1\0\1\7\1\u0ab2"+ + "\4\7\1\0\1\7\6\0\3\7\1\0\1\7\1\0"+ + "\1\7\1\0\15\7\2\0\2\7\11\0\1\7\1\0"+ + "\1\7\1\u0956\4\7\1\0\1\7\6\0\3\7\1\0"+ + "\1\7\1\0\1\7\1\0\15\7\2\0\2\7\11\0"+ + "\1\7\1\0\6\7\1\0\1\7\6\0\3\7\1\0"+ + "\1\7\1\0\1\7\1\u05df\1\u0640\14\7\2\0\2\7"+ + "\11\0\1\7\1\0\6\7\1\0\1\7\6\0\3\7"+ + "\1\0\1\7\1\0\1\7\1\u02f5\1\u02f6\14\7\2\0"+ + "\2\7\11\0\1\7\1\0\1\u02af\5\7\1\0\1\7"+ + "\6\0\3\7\1\0\1\7\1\0\1\7\1\0\15\7"+ + "\2\0\1\u010a\1\7\11\0\1\7\1\0\6\7\1\0"+ + "\1\7\6\0\3\7\1\0\1\7\1\0\1\7\1\0"+ + "\15\7\20\0\1\u0ab3\60\0\1\u0ab4\77\0\1\u0ab5\47\0"+ + "\1\u0ab6\104\0\1\u0ab7\57\0\2\u0ab8\37\0\1\u0ab9\75\0"+ + "\1\u0aba\41\0\1\u0abb\106\0\2\u06d3\16\0\2\7\11\0"+ + "\1\7\1\0\1\7\1\u0abc\4\7\1\0\1\7\6\0"+ + "\3\7\1\0\1\7\1\0\1\7\1\0\15\7\2\0"+ + "\2\7\11\0\1\7\1\0\1\u0abd\5\7\1\0\1\7"+ + "\6\0\3\7\1\0\1\7\1\0\1\7\1\0\15\7"+ + "\2\0\2\7\11\0\1\7\1\0\6\7\1\0\1\7"+ + "\6\0\1\u0abe\2\7\1\0\1\7\1\0\1\7\1\0"+ + "\15\7\2\0\2\7\11\0\1\7\1\0\4\7\1\u0abf"+ + "\1\7\1\0\1\7\6\0\3\7\1\0\1\7\1\0"+ + "\1\7\1\0\15\7\2\0\2\7\11\0\1\7\1\0"+ + "\6\7\1\0\1\7\6\0\3\7\1\0\1\7\1\0"+ + "\1\7\1\0\1\7\1\u0ac0\13\7\2\0\2\7\11\0"+ + "\1\7\1\0\6\7\1\0\1\7\6\0\3\7\1\0"+ + "\1\7\1\0\1\7\1\u0ab8\1\u0ac1\14\7\2\0\2\7"+ + "\11\0\1\7\1\0\4\7\1\u0ac2\1\7\1\0\1\7"+ + "\6\0\3\7\1\0\1\7\1\0\1\7\1\0\15\7"+ + "\2\0\2\7\11\0\1\7\1\0\6\7\1\0\1\7"+ + "\6\0\2\7\1\u0ac3\1\0\1\7\1\0\1\7\1\0"+ + "\15\7\2\0\2\7\11\0\1\7\1\0\1\u0ac4\5\7"+ + "\1\0\1\7\6\0\3\7\1\0\1\7\1\0\1\7"+ + "\1\0\15\7\2\0\2\7\11\0\1\7\1\0\6\7"+ + "\1\0\1\7\6\0\3\7\1\0\1\7\1\0\1\7"+ + "\1\u06d3\1\u072d\14\7\47\0\1\u0ac5\27\0\1\u0ac6\61\0"+ + "\1\u075a\63\0\1\u0237\101\0\1\u0ac7\57\0\1\u0a4f\70\0"+ + "\2\u0ac8\63\0\1\u0ac9\27\0\1\u0aca\46\0\2\7\11\0"+ + "\1\7\1\0\6\7\1\0\1\7\6\0\3\7\1\0"+ + "\1\7\1\0\1\7\1\0\2\7\1\u0acb\12\7\2\0"+ + "\2\7\11\0\1\u0acc\1\0\6\7\1\0\1\7\6\0"+ + "\3\7\1\0\1\7\1\0\1\7\1\0\15\7\2\0"+ + "\2\7\11\0\1\u01e5\1\0\6\7\1\0\1\7\6\0"+ + "\3\7\1\0\1\7\1\0\1\7\1\0\15\7\2\0"+ + "\2\7\11\0\1\7\1\0\1\326\5\7\1\0\1\7"+ + "\6\0\3\7\1\0\1\7\1\0\1\7\1\0\15\7"+ + "\2\0\2\7\11\0\1\7\1\0\6\7\1\0\1\7"+ + "\6\0\2\7\1\u0acd\1\0\1\7\1\0\1\7\1\0"+ + "\15\7\2\0\2\7\11\0\1\7\1\0\6\7\1\0"+ + "\1\7\6\0\1\u0a68\2\7\1\0\1\7\1\0\1\7"+ + "\1\0\15\7\2\0\2\7\11\0\1\7\1\0\6\7"+ + "\1\0\1\7\6\0\3\7\1\0\1\7\1\0\1\7"+ + "\1\u0ac8\1\u0ace\14\7\2\0\2\7\11\0\1\7\1\0"+ + "\6\7\1\0\1\7\6\0\3\7\1\0\1\7\1\0"+ + "\1\7\1\0\2\7\1\u0acf\12\7\2\0\2\7\11\0"+ + "\1\u0ad0\1\0\6\7\1\0\1\7\6\0\3\7\1\0"+ + "\1\7\1\0\1\7\1\0\15\7\23\0\1\u0ad1\56\0"+ + "\1\u0a4d\61\0\1\u0ad2\76\0\1\u0881\44\0\1\u0ad3\61\0"+ + "\1\u0976\43\0\2\7\11\0\1\7\1\0\4\7\1\u0ad4"+ + "\1\7\1\0\1\7\6\0\3\7\1\0\1\7\1\0"+ + "\1\7\1\0\15\7\2\0\2\7\11\0\1\7\1\0"+ + "\1\7\1\u0a66\4\7\1\0\1\7\6\0\3\7\1\0"+ + "\1\7\1\0\1\7\1\0\15\7\2\0\2\7\11\0"+ + "\1\7\1\0\1\7\1\u0ad5\4\7\1\0\1\7\6\0"+ + "\3\7\1\0\1\7\1\0\1\7\1\0\15\7\2\0"+ + "\2\7\11\0\1\7\1\0\6\7\1\0\1\7\6\0"+ + "\1\u026b\2\7\1\0\1\7\1\0\1\7\1\0\15\7"+ + "\2\0\2\7\11\0\1\7\1\0\1\7\1\u0ad6\4\7"+ + "\1\0\1\7\6\0\3\7\1\0\1\7\1\0\1\7"+ + "\1\0\15\7\2\0\2\7\11\0\1\7\1\0\1\7"+ + "\1\u09a5\4\7\1\0\1\7\6\0\3\7\1\0\1\7"+ + "\1\0\1\7\1\0\15\7\54\0\1\u0ad7\43\0\1\u0ad8"+ + "\40\0\1\u0ad9\46\0\2\7\11\0\1\7\1\0\6\7"+ + "\1\0\1\7\6\0\3\7\1\0\1\7\1\0\1\7"+ + "\1\0\7\7\1\u0ada\5\7\2\0\2\7\11\0\1\7"+ + "\1\0\6\7\1\0\1\7\6\0\1\7\1\u0adb\1\7"+ + "\1\0\1\7\1\0\1\7\1\0\15\7\2\0\2\7"+ + "\11\0\1\u0adc\1\0\6\7\1\0\1\7\6\0\3\7"+ + "\1\0\1\7\1\0\1\7\1\0\15\7\15\0\1\u0add"+ + "\61\0\1\u0ade\71\0\2\u01dc\35\0\2\7\11\0\1\u0adf"+ + "\1\0\6\7\1\0\1\7\6\0\3\7\1\0\1\7"+ + "\1\0\1\7\1\0\15\7\2\0\2\7\11\0\1\u0ae0"+ + "\1\0\6\7\1\0\1\7\6\0\3\7\1\0\1\7"+ + "\1\0\1\7\1\0\15\7\2\0\2\7\11\0\1\7"+ + "\1\0\6\7\1\u01dc\1\u01dd\6\0\3\7\1\0\1\7"+ + "\1\0\1\7\1\0\15\7\36\0\1\u0ae1\76\0\1\u0ae2"+ + "\10\0\2\7\11\0\1\7\1\0\6\7\1\0\1\7"+ + "\6\0\1\7\1\u0ae3\1\7\1\0\1\7\1\0\1\7"+ + "\1\0\15\7\2\0\2\7\11\0\1\7\1\0\6\7"+ + "\1\0\1\7\6\0\3\7\1\0\1\7\1\0\1\7"+ + "\1\0\6\7\1\u0ae4\6\7\15\0\1\u0ae5\114\0\1\u0ae6"+ + "\13\0\2\7\11\0\1\u0ae7\1\0\6\7\1\0\1\7"+ + "\6\0\3\7\1\0\1\7\1\0\1\7\1\0\15\7"+ + "\2\0\2\7\11\0\1\7\1\0\6\7\1\0\1\7"+ + "\6\0\3\7\1\0\1\7\1\0\1\7\1\0\3\7"+ + "\1\u0ae8\11\7\17\0\1\u0ae9\101\0\1\u0aea\24\0\2\7"+ + "\11\0\1\7\1\0\1\u0aeb\5\7\1\0\1\7\6\0"+ + "\3\7\1\0\1\7\1\0\1\7\1\0\15\7\2\0"+ + "\2\7\11\0\1\7\1\0\6\7\1\0\1\7\6\0"+ + "\2\7\1\u0aec\1\0\1\7\1\0\1\7\1\0\15\7"+ + "\23\0\1\u0aed\56\0\1\u0aee\43\0\2\7\11\0\1\7"+ + "\1\0\4\7\1\u0aef\1\7\1\0\1\7\6\0\3\7"+ + "\1\0\1\7\1\0\1\7\1\0\15\7\2\0\2\7"+ + "\11\0\1\7\1\0\1\7\1\u0af0\4\7\1\0\1\7"+ + "\6\0\3\7\1\0\1\7\1\0\1\7\1\0\15\7"+ + "\44\0\2\u0af1\33\0\1\u0a4d\44\0\2\7\11\0\1\7"+ + "\1\0\6\7\1\0\1\7\6\0\3\7\1\0\1\7"+ + "\1\0\1\7\1\u0af1\1\u0af2\14\7\2\0\2\7\11\0"+ + "\1\7\1\0\1\u0a66\5\7\1\0\1\7\6\0\3\7"+ + "\1\0\1\7\1\0\1\7\1\0\15\7\24\0\1\u01dc"+ + "\37\0\2\7\11\0\1\7\1\0\5\7\1\u01dd\1\0"+ + "\1\7\6\0\3\7\1\0\1\7\1\0\1\7\1\0"+ + "\15\7\1\0"; + + private static int [] zzUnpackTrans() { + int [] result = new int[138450]; + int offset = 0; + offset = zzUnpackTrans(ZZ_TRANS_PACKED_0, offset, result); + offset = zzUnpackTrans(ZZ_TRANS_PACKED_1, offset, result); + return result; + } + + private static int zzUnpackTrans(String packed, int offset, int [] result) { + int i = 0; /* index in packed string */ + int j = offset; /* index in unpacked array */ + int l = packed.length(); + while (i < l) { + int count = packed.charAt(i++); + int value = packed.charAt(i++); + value--; + do result[j++] = value; while (--count > 0); + } + return j; + } + + + /* error codes */ + private static final int ZZ_UNKNOWN_ERROR = 0; + private static final int ZZ_NO_MATCH = 1; + private static final int ZZ_PUSHBACK_2BIG = 2; + + /* error messages for the codes above */ + private static final String ZZ_ERROR_MSG[] = { + "Unkown internal scanner error", + "Error: could not match input", + "Error: pushback value was too large" + }; + + /** + * ZZ_ATTRIBUTE[aState] contains the attributes of state aState + */ + private static final int [] ZZ_ATTRIBUTE = zzUnpackAttribute(); + + private static final String ZZ_ATTRIBUTE_PACKED_0 = + "\5\0\1\11\2\1\1\11\2\1\1\11\1\1\1\11"+ + "\3\1\1\11\16\1\1\11\16\1\1\11\1\1\1\11"+ + "\2\1\2\11\1\1\1\11\2\1\1\11\1\1\1\11"+ + "\4\1\1\0\10\1\1\0\6\1\1\0\1\1\1\0"+ + "\7\1\1\0\3\1\1\0\7\1\1\0\1\1\12\0"+ + "\12\1\3\0\4\1\1\0\14\1\1\0\1\1\1\0"+ + "\1\11\1\1\3\0\25\1\1\0\2\1\1\0\12\1"+ + "\2\11\1\1\1\0\1\1\1\0\1\1\1\0\3\1"+ + "\1\0\6\1\1\0\10\1\1\0\6\1\1\0\1\1"+ + "\2\0\13\1\1\0\1\1\1\0\1\1\1\0\6\1"+ + "\20\0\17\1\4\0\17\1\1\0\1\1\7\0\11\1"+ + "\1\0\7\1\1\0\3\1\1\0\5\1\1\0\5\1"+ + "\2\0\3\1\1\0\1\1\1\0\4\1\1\0\6\1"+ + "\1\11\3\1\1\0\3\1\1\11\4\1\1\0\4\1"+ + "\1\0\5\1\1\0\4\1\4\0\10\1\1\0\2\1"+ + "\1\0\1\1\1\0\2\1\7\0\1\1\7\0\16\1"+ + "\4\0\2\1\1\0\2\1\1\0\6\1\1\0\3\1"+ + "\1\0\1\1\3\0\4\1\1\0\6\1\1\0\1\1"+ + "\1\0\5\1\1\0\5\1\1\0\3\1\1\0\1\1"+ + "\1\0\4\1\1\0\1\1\1\0\3\1\1\0\5\1"+ + "\1\0\1\1\1\0\3\1\1\0\3\1\1\0\4\1"+ + "\4\0\5\1\1\11\1\0\1\1\15\0\14\1\1\11"+ + "\2\0\1\1\2\0\2\1\1\0\2\1\4\0\3\1"+ + "\1\0\1\1\1\0\3\1\1\0\2\1\1\11\4\1"+ + "\1\0\3\1\2\0\3\1\1\0\2\1\1\0\2\1"+ + "\1\0\2\1\1\0\1\1\1\0\1\1\1\0\2\1"+ + "\1\0\2\1\3\0\3\1\1\0\3\1\6\0\1\1"+ + "\3\0\11\1\3\0\2\1\1\0\1\1\1\0\1\1"+ + "\3\0\3\1\1\0\1\1\1\0\2\1\1\0\5\1"+ + "\2\0\2\1\1\0\3\1\1\0\2\1\1\0\1\1"+ + "\1\0\1\1\1\0\1\1\1\0\1\1\3\0\1\1"+ + "\1\0\1\1\10\0\7\1\6\0\6\1\1\0\1\1"+ + "\5\0\5\1\1\0\2\1\1\0\1\1\1\0\2\1"+ + "\2\0\2\1\1\0\2\1\1\0\2\1\1\0\1\1"+ + "\1\0\1\1\2\0\1\1\10\0\5\1\1\0\1\1"+ + "\2\0\4\1\1\0\1\1\4\0\5\1\1\0\1\1"+ + "\2\0\2\1\1\0\3\1\1\0\1\1\2\0\2\1"+ + "\7\0\2\1\4\0\1\1\1\0\1\1\3\0\3\1"+ + "\1\0\1\1\1\0\2\1\1\0\1\1\2\0\2\1"+ + "\2\0\1\1\2\0\1\1\22\0\23\1\1\0\1\1"+ + "\1\0\1\1\1\0\1\1\2\0\2\1\2\0\1\1"+ + "\53\0\54\1\1\0\1\1\2\0\2\1\1\0\1\1"+ + "\73\0\73\1\1\0\1\1\2\0\2\1\1\0\1\1"+ + "\122\0\122\1\1\0\1\1\143\0\143\1\14\0\1\1"+ + "\124\0\141\1\133\0\132\1\125\0\122\1\110\0\105\1"+ + "\102\0\77\1\71\0\67\1\57\0\55\1\44\0\44\1"+ + "\33\0\1\1\1\0\35\1\31\0\31\1\21\0\21\1"+ + "\12\0\12\1\11\0\11\1\6\0\6\1\3\0\3\1"+ + "\3\0\3\1\2\0\2\1\2\0\2\1\2\0\2\1"+ + "\2\0\2\1\2\0\2\1\1\0\1\1"; + + private static int [] zzUnpackAttribute() { + int [] result = new int[2802]; + int offset = 0; + offset = zzUnpackAttribute(ZZ_ATTRIBUTE_PACKED_0, offset, result); + return result; + } + + private static int zzUnpackAttribute(String packed, int offset, int [] result) { + int i = 0; /* index in packed string */ + int j = offset; /* index in unpacked array */ + int l = packed.length(); + while (i < l) { + int count = packed.charAt(i++); + int value = packed.charAt(i++); + do result[j++] = value; while (--count > 0); + } + return j; + } + + /** the input device */ + private java.io.Reader zzReader; + + /** the current state of the DFA */ + private int zzState; + + /** the current lexical state */ + private int zzLexicalState = YYINITIAL; + + /** this buffer contains the current text to be matched and is + the source of the yytext() string */ + private char[] zzBuffer; + + /** the textposition at the last accepting state */ + private int zzMarkedPos; + + /** the current text position in the buffer */ + private int zzCurrentPos; + + /** startRead marks the beginning of the yytext() string in the buffer */ + private int zzStartRead; + + /** endRead marks the last character in the buffer, that has been read + from input */ + private int zzEndRead; + + /** zzAtEOF == true <=> the scanner is at the EOF */ + private boolean zzAtEOF; + + + + /* user code: */ + + /** Style for highlighting MENU scripts. */ + public static final String SYNTAX_STYLE_MENU = "text/MENU"; + + /** + * Constructor. This must be here because JFlex does not generate a + * no-parameter constructor. + */ + public MenuTokenMaker() { + } + + + /** + * Adds the token specified to the current linked list of tokens. + * + * @param tokenType The token's type. + */ + private void addToken(int tokenType) { + addToken(zzStartRead, zzMarkedPos-1, tokenType); + } + + + /** + * Adds the token specified to the current linked list of tokens. + * + * @param tokenType The token's type. + */ + private void addToken(int start, int end, int tokenType) { + int so = start + offsetShift; + addToken(zzBuffer, start,end, tokenType, so); + } + + + /** + * Adds the token specified to the current linked list of tokens. + * + * @param array The character array. + * @param start The starting offset in the array. + * @param end The ending offset in the array. + * @param tokenType The token's type. + * @param startOffset The offset in the document at which this token + * occurs. + */ + @Override + public void addToken(char[] array, int start, int end, int tokenType, int startOffset) { + super.addToken(array, start,end, tokenType, startOffset); + zzStartRead = zzMarkedPos; + } + + + @Override + public String[] getLineCommentStartAndEnd(int languageIndex) { + return new String[] { "--", null }; + } + + + /** + * Returns the first token in the linked list of tokens generated + * from text. This method must be implemented by + * subclasses so they can correctly implement syntax highlighting. + * + * @param text The text from which to get tokens. + * @param initialTokenType The token type we should start with. + * @param startOffset The offset into the document at which + * text starts. + * @return The first Token in a linked list representing + * the syntax highlighted text. + */ + public Token getTokenList(Segment text, int initialTokenType, int startOffset) { + + resetTokenList(); + this.offsetShift = -text.offset + startOffset; + + // Start off in the proper state. + int state = Token.NULL; + switch (initialTokenType) { + case Token.COMMENT_MULTILINE: + state = MLC; + start = text.offset; + break; + case Token.LITERAL_BACKQUOTE: + state = LONGSTRING; + start = text.offset; + break; + case Token.LITERAL_STRING_DOUBLE_QUOTE: + state = STRING; + start = text.offset; + break; + default: + state = Token.NULL; + } + + s = text; + try { + yyreset(zzReader); + yybegin(state); + return yylex(); + } catch (IOException ioe) { + ioe.printStackTrace(); + return new TokenImpl(); + } + + } + + + /** + * Refills the input buffer. + * + * @return true if EOF was reached, otherwise + * false. + */ + private boolean zzRefill() { + return zzCurrentPos>=s.offset+s.count; + } + + + /** + * Resets the scanner to read from a new input stream. + * Does not close the old reader. + * + * All internal variables are reset, the old input stream + * cannot be reused (internal buffer is discarded and lost). + * Lexical state is set to YY_INITIAL. + * + * @param reader the new input stream + */ + public final void yyreset(Reader reader) { + // 's' has been updated. + zzBuffer = s.array; + /* + * We replaced the line below with the two below it because zzRefill + * no longer "refills" the buffer (since the way we do it, it's always + * "full" the first time through, since it points to the segment's + * array). So, we assign zzEndRead here. + */ + //zzStartRead = zzEndRead = s.offset; + zzStartRead = s.offset; + zzEndRead = zzStartRead + s.count - 1; + zzCurrentPos = zzMarkedPos = s.offset; + zzLexicalState = YYINITIAL; + zzReader = reader; + zzAtEOF = false; + } + + + + + /** + * Creates a new scanner + * There is also a java.io.InputStream version of this constructor. + * + * @param in the java.io.Reader to read input from. + */ + public MenuTokenMaker(java.io.Reader in) { + this.zzReader = in; + } + + /** + * Creates a new scanner. + * There is also java.io.Reader version of this constructor. + * + * @param in the java.io.Inputstream to read input from. + */ + public MenuTokenMaker(java.io.InputStream in) { + this(new java.io.InputStreamReader + (in, java.nio.charset.Charset.forName("UTF-8"))); + } + + /** + * Unpacks the compressed character translation table. + * + * @param packed the packed character translation table + * @return the unpacked character translation table + */ + private static char [] zzUnpackCMap(String packed) { + char [] map = new char[0x10000]; + int i = 0; /* index in packed string */ + int j = 0; /* index in unpacked array */ + while (i < 204) { + int count = packed.charAt(i++); + char value = packed.charAt(i++); + do map[j++] = value; while (--count > 0); + } + return map; + } + + + /** + * Closes the input stream. + */ + public final void yyclose() throws java.io.IOException { + zzAtEOF = true; /* indicate end of file */ + zzEndRead = zzStartRead; /* invalidate buffer */ + + if (zzReader != null) + zzReader.close(); + } + + + /** + * Returns the current lexical state. + */ + public final int yystate() { + return zzLexicalState; + } + + + /** + * Enters a new lexical state + * + * @param newState the new lexical state + */ + public final void yybegin(int newState) { + zzLexicalState = newState; + } + + + /** + * Returns the text matched by the current regular expression. + */ + public final String yytext() { + return new String( zzBuffer, zzStartRead, zzMarkedPos-zzStartRead ); + } + + + /** + * Returns the character at position pos from the + * matched text. + * + * It is equivalent to yytext().charAt(pos), but faster + * + * @param pos the position of the character to fetch. + * A value from 0 to yylength()-1. + * + * @return the character at position pos + */ + public final char yycharat(int pos) { + return zzBuffer[zzStartRead+pos]; + } + + + /** + * Returns the length of the matched text region. + */ + public final int yylength() { + return zzMarkedPos-zzStartRead; + } + + + /** + * Reports an error that occured while scanning. + * + * In a wellformed scanner (no or only correct usage of + * yypushback(int) and a match-all fallback rule) this method + * will only be called with things that "Can't Possibly Happen". + * If this method is called, something is seriously wrong + * (e.g. a JFlex bug producing a faulty scanner etc.). + * + * Usual syntax/scanner level error handling should be done + * in error fallback rules. + * + * @param errorCode the code of the errormessage to display + */ + private void zzScanError(int errorCode) { + String message; + try { + message = ZZ_ERROR_MSG[errorCode]; + } + catch (ArrayIndexOutOfBoundsException e) { + message = ZZ_ERROR_MSG[ZZ_UNKNOWN_ERROR]; + } + + throw new Error(message); + } + + + /** + * Pushes the specified amount of characters back into the input stream. + * + * They will be read again by then next call of the scanning method + * + * @param number the number of characters to be read again. + * This number must not be greater than yylength()! + */ + public void yypushback(int number) { + if ( number > yylength() ) + zzScanError(ZZ_PUSHBACK_2BIG); + + zzMarkedPos -= number; + } + + + /** + * Resumes scanning until the next regular expression is matched, + * the end of input is encountered or an I/O-Error occurs. + * + * @return the next token + * @exception java.io.IOException if any I/O-Error occurs + */ + public org.fife.ui.rsyntaxtextarea.Token yylex() throws java.io.IOException { + int zzInput; + int zzAction; + + // cached fields: + int zzCurrentPosL; + int zzMarkedPosL; + int zzEndReadL = zzEndRead; + char [] zzBufferL = zzBuffer; + char [] zzCMapL = ZZ_CMAP; + + int [] zzTransL = ZZ_TRANS; + int [] zzRowMapL = ZZ_ROWMAP; + int [] zzAttrL = ZZ_ATTRIBUTE; + + while (true) { + zzMarkedPosL = zzMarkedPos; + + zzAction = -1; + + zzCurrentPosL = zzCurrentPos = zzStartRead = zzMarkedPosL; + + zzState = ZZ_LEXSTATE[zzLexicalState]; + + // set up zzAction for empty match case: + int zzAttributes = zzAttrL[zzState]; + if ( (zzAttributes & 1) == 1 ) { + zzAction = zzState; + } + + + zzForAction: { + while (true) { + + if (zzCurrentPosL < zzEndReadL) + zzInput = zzBufferL[zzCurrentPosL++]; + else if (zzAtEOF) { + zzInput = YYEOF; + break zzForAction; + } + else { + // store back cached positions + zzCurrentPos = zzCurrentPosL; + zzMarkedPos = zzMarkedPosL; + boolean eof = zzRefill(); + // get translated positions and possibly new buffer + zzCurrentPosL = zzCurrentPos; + zzMarkedPosL = zzMarkedPos; + zzBufferL = zzBuffer; + zzEndReadL = zzEndRead; + if (eof) { + zzInput = YYEOF; + break zzForAction; + } + else { + zzInput = zzBufferL[zzCurrentPosL++]; + } + } + int zzNext = zzTransL[ zzRowMapL[zzState] + zzCMapL[zzInput] ]; + if (zzNext == -1) break zzForAction; + zzState = zzNext; + + zzAttributes = zzAttrL[zzState]; + if ( (zzAttributes & 1) == 1 ) { + zzAction = zzState; + zzMarkedPosL = zzCurrentPosL; + if ( (zzAttributes & 8) == 8 ) break zzForAction; + } + + } + } + + // store back cached position + zzMarkedPos = zzMarkedPosL; + + switch (zzAction < 0 ? zzAction : ZZ_ACTION[zzAction]) { + case 1: + { addToken(Token.IDENTIFIER); + } + case 27: break; + case 2: + { addToken(Token.LITERAL_NUMBER_FLOAT); + } + case 28: break; + case 3: + { addNullToken(); return firstToken; + } + case 29: break; + case 4: + { addToken(Token.WHITESPACE); + } + case 30: break; + case 5: + { addToken(Token.ERROR_CHAR); addNullToken(); return firstToken; + } + case 31: break; + case 6: + { start = zzMarkedPos-1; yybegin(STRING); + } + case 32: break; + case 7: + { addToken(Token.SEPARATOR); + } + case 33: break; + case 8: + { addToken(Token.OPERATOR); + } + case 34: break; + case 9: + { addToken(Token.PREPROCESSOR); + } + case 35: break; + case 10: + { + } + case 36: break; + case 11: + { addToken(start,zzStartRead-1, Token.COMMENT_MULTILINE); return firstToken; + } + case 37: break; + case 12: + { addToken(start,zzStartRead-1, Token.LITERAL_BACKQUOTE); return firstToken; + } + case 38: break; + case 13: + { addToken(start,zzStartRead-1, Token.LITERAL_STRING_DOUBLE_QUOTE); return firstToken; + } + case 39: break; + case 14: + { yybegin(YYINITIAL); addToken(start,zzStartRead, Token.LITERAL_STRING_DOUBLE_QUOTE); + } + case 40: break; + case 15: + { addToken(start,zzStartRead-1, Token.COMMENT_EOL); return firstToken; + } + case 41: break; + case 16: + { addToken(Token.LITERAL_CHAR); + } + case 42: break; + case 17: + { start = zzMarkedPos-2; yybegin(LONGSTRING); + } + case 43: break; + case 18: + { start = zzMarkedPos-2; yybegin(LINECOMMENT); + } + case 44: break; + case 19: + { addToken(Token.RESERVED_WORD); + } + case 45: break; + case 20: + { addToken(Token.RESERVED_WORD_2); + } + case 46: break; + case 21: + { addToken(Token.FUNCTION); + } + case 47: break; + case 22: + { yybegin(YYINITIAL); addToken(start,zzStartRead+1, Token.COMMENT_MULTILINE); + } + case 48: break; + case 23: + { yybegin(YYINITIAL); addToken(start,zzStartRead+1, Token.LITERAL_BACKQUOTE); + } + case 49: break; + case 24: + { start = zzMarkedPos-4; yybegin(MLC); + } + case 50: break; + case 25: + { addToken(Token.LITERAL_BOOLEAN); + } + case 51: break; + case 26: + { addToken(Token.DATA_TYPE); + } + case 52: break; + default: + if (zzInput == YYEOF && zzStartRead == zzCurrentPos) { + zzAtEOF = true; + switch (zzLexicalState) { + case YYINITIAL: { + addNullToken(); return firstToken; + } + case 2803: break; + case MLC: { + addToken(start,zzStartRead-1, Token.COMMENT_MULTILINE); return firstToken; + } + case 2804: break; + case LONGSTRING: { + addToken(start,zzStartRead-1, Token.LITERAL_BACKQUOTE); return firstToken; + } + case 2805: break; + case STRING: { + addToken(start,zzStartRead-1, Token.LITERAL_STRING_DOUBLE_QUOTE); return firstToken; + } + case 2806: break; + case LINECOMMENT: { + addToken(start,zzStartRead-1, Token.COMMENT_EOL); return firstToken; + } + case 2807: break; + default: + return null; + } + } + else { + zzScanError(ZZ_NO_MATCH); + } + } + } + } + + +} diff --git a/src/org/infinity/resource/to/StrRefEntry.java b/src/org/infinity/resource/to/StrRefEntry.java index d7c783579..e7d28c5e2 100644 --- a/src/org/infinity/resource/to/StrRefEntry.java +++ b/src/org/infinity/resource/to/StrRefEntry.java @@ -6,10 +6,12 @@ import java.nio.ByteBuffer; +import org.infinity.datatype.DecNumber; +import org.infinity.datatype.Flag; import org.infinity.datatype.HexNumber; import org.infinity.datatype.ResourceRef; import org.infinity.datatype.StringRef; -import org.infinity.datatype.Unknown; +import org.infinity.gui.StringEditor; import org.infinity.resource.AbstractStruct; import org.infinity.util.io.StreamUtils; @@ -17,7 +19,6 @@ public class StrRefEntry extends AbstractStruct { // TOH/StrrefEntry-specific field labels public static final String TOH_STRREF = "StrRef entry"; public static final String TOH_STRREF_OVERRIDDEN = "Overridden strref"; - public static final String TOH_STRREF_SOUND = "Associated sound"; public static final String TOH_STRREF_OFFSET_TOT_STRING = "TOT string offset"; public StrRefEntry() throws Exception { @@ -35,10 +36,10 @@ public StrRefEntry(AbstractStruct superStruct, String name, ByteBuffer buffer, i @Override public int read(ByteBuffer buffer, int offset) throws Exception { addField(new StringRef(buffer, offset, TOH_STRREF_OVERRIDDEN)); - addField(new Unknown(buffer, offset + 4, 4)); - addField(new Unknown(buffer, offset + 8, 4)); - addField(new Unknown(buffer, offset + 12, 4)); - addField(new ResourceRef(buffer, offset + 16, TOH_STRREF_SOUND, "WAV")); + addField(new Flag(buffer, offset + 4, 4, StringEditor.TLK_FLAGS, StringEditor.FLAGS)); + addField(new ResourceRef(buffer, offset + 8, StringEditor.TLK_SOUND, "WAV")); + addField(new DecNumber(buffer, offset + 16, 4, StringEditor.TLK_VOLUME)); + addField(new DecNumber(buffer, offset + 20, 4, StringEditor.TLK_PITCH)); addField(new HexNumber(buffer, offset + 24, 4, TOH_STRREF_OFFSET_TOT_STRING)); return offset + 28; } diff --git a/src/org/infinity/resource/to/TohResource.java b/src/org/infinity/resource/to/TohResource.java index 09ea40170..0956b3630 100644 --- a/src/org/infinity/resource/to/TohResource.java +++ b/src/org/infinity/resource/to/TohResource.java @@ -195,10 +195,17 @@ public static String getOverrideString(TohResource toh, TotResource tot, int str if (strrefEntry != null) { int v = ((IsNumeric) strrefEntry.getAttribute(StrRefEntry.TOH_STRREF_OVERRIDDEN)).getValue(); if (v == strref) { + // string entry may consist of multiple segments + retVal = ""; int sofs = ((IsNumeric) strrefEntry.getAttribute(StrRefEntry.TOH_STRREF_OFFSET_TOT_STRING)).getValue(); - StringEntry se = (StringEntry) tot.getAttribute(sofs, false); - if (se != null) { - retVal = se.getAttribute(StringEntry.TOT_STRING_TEXT).toString(); + while (sofs >= 0) { + StringEntry se = (StringEntry) tot.getAttribute(sofs, false); + if (se != null) { + retVal += se.getAttribute(StringEntry.TOT_STRING_TEXT).toString(); + sofs = ((IsNumeric) se.getAttribute(StringEntry.TOT_STRING_OFFSET_NEXT_ENTRY)).getValue(); + } else { + sofs = -1; + } } break; } diff --git a/src/org/infinity/resource/to/TotResource.java b/src/org/infinity/resource/to/TotResource.java index 5d6703e36..5fb29d93c 100644 --- a/src/org/infinity/resource/to/TotResource.java +++ b/src/org/infinity/resource/to/TotResource.java @@ -76,12 +76,15 @@ public int read(ByteBuffer buffer, int offset) throws Exception { // looping through entries to consider split text segments StringEntry stringEntry = new StringEntry(this, buffer, offset, idx); while (stringEntry != null) { + if (!isStringEntryValid(stringEntry)) { + throw new Exception(String.format("Invalid string section found at offset 0x%x", stringEntry.getOffset())); + } offset = stringEntry.getEndOffset(); addField(stringEntry); idx++; int ofsNextEntry = ((IsNumeric) stringEntry.getAttribute(StringEntry.TOT_STRING_OFFSET_NEXT_ENTRY)) .getValue(); - if (ofsNextEntry >= 0) { + if (ofsNextEntry != -1) { stringEntry = new StringEntry(this, buffer, ofsNextEntry, idx); } else { stringEntry = null; @@ -110,6 +113,28 @@ public int read(ByteBuffer buffer, int offset) throws Exception { return endoffset; } + /** + * Analyzes a TOT {@code StringEntry} to determine whether the entry contains valid data. + * + * @param entry a {@link StringEntry} + * @return {@code true} if the entry is valid, {@code false} otherwise. + */ + public static boolean isStringEntryValid(StringEntry entry) { + boolean retVal = false; + + if (entry != null) { + try { + int ofsPrev = ((IsNumeric) entry.getAttribute(StringEntry.TOT_STRING_OFFSET_PREV_ENTRY)).getValue(); + int ofsNext = ((IsNumeric) entry.getAttribute(StringEntry.TOT_STRING_OFFSET_NEXT_ENTRY)).getValue(); + retVal = (ofsPrev == -1 && ofsNext == -1) || (ofsPrev != ofsNext); + } catch (Exception e) { + e.printStackTrace(); + } + } + + return retVal; + } + /** * Attempts to find the associated TOH resource in the same directory as the current TOT resource and loads it if * available. diff --git a/src/org/infinity/resource/wmp/ViewerMap.java b/src/org/infinity/resource/wmp/ViewerMap.java index 7ba4e3c28..3eb51cfbe 100644 --- a/src/org/infinity/resource/wmp/ViewerMap.java +++ b/src/org/infinity/resource/wmp/ViewerMap.java @@ -168,8 +168,8 @@ private enum Direction { mapScaleX = (float) mapOrig.getWidth() / (float) mapTargetWidth; mapScaleY = (float) mapOrig.getHeight() / (float) mapTargetHeight; - listPanel = (StructListPanel) ViewerUtil.makeListPanel("Areas", wmpMap, AreaEntry.class, - AreaEntry.WMP_AREA_CURRENT, new WmpAreaListRenderer(mapIcons), listeners); + listPanel = ViewerUtil.makeListPanel("Areas", wmpMap, AreaEntry.class, AreaEntry.WMP_AREA_CURRENT, + new WmpAreaListRenderer(mapIcons), listeners); listPanel.getList().setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION); JScrollPane mapScroll = new JScrollPane(rcMap); mapScroll.getVerticalScrollBar().setUnitIncrement(16); @@ -617,7 +617,7 @@ private void resetMap() { Graphics2D g = ((BufferedImage) rcMap.getImage()).createGraphics(); try { Composite comp = g.getComposite(); - g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC)); + g.setComposite(AlphaComposite.Src); g.drawImage(mapOrig, 0, 0, null); g.setComposite(comp); } finally { diff --git a/src/org/infinity/search/AbstractSearcher.java b/src/org/infinity/search/AbstractSearcher.java index 7096505a5..c7bf60694 100644 --- a/src/org/infinity/search/AbstractSearcher.java +++ b/src/org/infinity/search/AbstractSearcher.java @@ -6,7 +6,7 @@ import java.awt.Component; import java.util.List; -import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; import javax.swing.JOptionPane; import javax.swing.ProgressMonitor; @@ -15,6 +15,7 @@ import org.infinity.resource.key.ResourceEntry; import org.infinity.util.Debugging; import org.infinity.util.Misc; +import org.infinity.util.Threading; /** * Utility class for performing searching of resources in several threads with ability to cancel search. @@ -90,48 +91,50 @@ protected boolean runSearch(String operation, List entries) { lastExt = entries.get(0).getExtension(); updateProgressNote(); - final ThreadPoolExecutor executor = Misc.createThreadPool(); boolean isCancelled = false; - Debugging.timerReset(); - int i = 0; - for (final ResourceEntry entry : entries) { - if (progress.isCanceled()) { - break; - } - if (entry == null) { - ++i; - advanceProgress(false); - continue; - } - if (i++ % 10 == 0) { - final String ext = entry.getExtension(); - if (!lastExt.equalsIgnoreCase(ext)) { - lastExt = ext; - updateProgressNote(); + try (final Threading threadPool = new Threading()) { + Debugging.timerReset(); + int i = 0; + for (final ResourceEntry entry : entries) { + if (progress.isCanceled()) { + break; + } + if (entry == null) { + ++i; + advanceProgress(false); + continue; + } + if (i++ % 10 == 0) { + final String ext = entry.getExtension(); + if (!lastExt.equalsIgnoreCase(ext)) { + lastExt = ext; + updateProgressNote(); + } } - } - - Misc.isQueueReady(executor, true, -1); - executor.execute(newWorker(entry)); - } - // enforcing thread termination if process has been cancelled - if (isCancelled) { - executor.shutdownNow(); - } else { - executor.shutdown(); - } + threadPool.submit(newWorker(entry)); + } - // waiting for pending threads to terminate - while (!executor.isTerminated()) { - if (!isCancelled && progress.isCanceled()) { - executor.shutdownNow(); - isCancelled = true; + // enforcing thread termination if process has been cancelled + if (isCancelled) { + threadPool.shutdownNow(); + } else { + threadPool.shutdown(); } - try { - Thread.sleep(1); - } catch (InterruptedException e) { + + // waiting for pending threads to terminate + while (!threadPool.isTerminated()) { + if (!isCancelled && progress.isCanceled()) { + threadPool.shutdownNow(); + isCancelled = true; + } + try { + threadPool.awaitTermination(10L, TimeUnit.MILLISECONDS); + } catch (InterruptedException e) { + } } + } catch (Exception e) { + // ignored } Debugging.timerShow(operation + " completed", Debugging.TimeFormat.MILLISECONDS); diff --git a/src/org/infinity/search/SearchResource.java b/src/org/infinity/search/SearchResource.java index 149386112..cc6ee3f04 100644 --- a/src/org/infinity/search/SearchResource.java +++ b/src/org/infinity/search/SearchResource.java @@ -28,7 +28,6 @@ import java.util.Map; import java.util.SortedMap; import java.util.Vector; -import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import javax.swing.BorderFactory; @@ -97,6 +96,7 @@ import org.infinity.util.IdsMapEntry; import org.infinity.util.Misc; import org.infinity.util.SimpleListModel; +import org.infinity.util.Threading; import org.infinity.util.io.StreamUtils; import org.infinity.util.tuples.Couple; @@ -228,18 +228,20 @@ public void run() { SearchOptions so = panel.getOptions(); // using parallel jobs to speed up search - ThreadPoolExecutor executor = Misc.createThreadPool(); - for (ResourceEntry element : resources) { - Misc.isQueueReady(executor, true, -1); - executor.execute(new SearchWorker(found, so, element)); - } + try (final Threading threadPool = new Threading()) { + for (ResourceEntry element : resources) { + threadPool.submit(new SearchWorker(found, so, element)); + } - // waiting for threads to finish - executor.shutdown(); - try { - executor.awaitTermination(60, TimeUnit.SECONDS); - } catch (InterruptedException e) { - e.printStackTrace(); + // waiting for threads to finish + threadPool.shutdown(); + try { + threadPool.awaitTermination(60, TimeUnit.SECONDS); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } catch (Exception e) { + // ignored } // preparing results for output diff --git a/src/org/infinity/search/advanced/AdvancedSearch.java b/src/org/infinity/search/advanced/AdvancedSearch.java index 5796bf657..70482bb7a 100644 --- a/src/org/infinity/search/advanced/AdvancedSearch.java +++ b/src/org/infinity/search/advanced/AdvancedSearch.java @@ -27,7 +27,6 @@ import java.util.List; import java.util.Map; import java.util.Vector; -import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; @@ -78,6 +77,7 @@ import org.infinity.util.Debugging; import org.infinity.util.Misc; import org.infinity.util.SimpleListModel; +import org.infinity.util.Threading; public class AdvancedSearch extends ChildFrame implements Runnable { /** Indicates how to evaluate filter matches against a resource. */ @@ -560,18 +560,20 @@ public void run() { List searchOptions = getSearchOptions(); // using parallel jobs to speed up search - ThreadPoolExecutor executor = Misc.createThreadPool(); - for (final ResourceEntry entry : resources) { - Misc.isQueueReady(executor, true, -1); - executor.execute(new AdvancedSearchWorker(found, filterOp, searchOptions, entry, pbProgress)); - } + try (final Threading threadPool = new Threading()) { + for (final ResourceEntry entry : resources) { + threadPool.submit(new AdvancedSearchWorker(found, filterOp, searchOptions, entry, pbProgress)); + } - // waiting for threads to finish - executor.shutdown(); - try { - executor.awaitTermination(60, TimeUnit.SECONDS); - } catch (InterruptedException e) { - e.printStackTrace(); + // waiting for threads to finish + threadPool.shutdown(); + try { + threadPool.awaitTermination(60, TimeUnit.SECONDS); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } catch (Exception e) { + // ignored } // preparing results for output diff --git a/src/org/infinity/util/BinPack2D.java b/src/org/infinity/util/BinPack2D.java index 891c64e2b..4ba1a54f0 100644 --- a/src/org/infinity/util/BinPack2D.java +++ b/src/org/infinity/util/BinPack2D.java @@ -9,6 +9,7 @@ import java.util.ArrayList; import java.util.Iterator; import java.util.List; +import java.util.Objects; import org.infinity.util.tuples.Couple; @@ -246,6 +247,34 @@ public float getOccupancy() { return (float) (usedSurfaceArea) / (float) (binWidth * binHeight); } + @Override + public int hashCode() { + return Objects.hash(binHeight, binWidth, freeRectangles, usedRectangles); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + BinPack2D other = (BinPack2D)obj; + return binHeight == other.binHeight && binWidth == other.binWidth + && Objects.equals(freeRectangles, other.freeRectangles) && Objects.equals(usedRectangles, other.usedRectangles); + } + + + @Override + public String toString() { + return "BinPack2D [binWidth=" + binWidth + ", binHeight=" + binHeight + ", usedRectangles=" + usedRectangles + + ", freeRectangles=" + freeRectangles + "]"; + } + /** * Computes the placement score for placing the given rectangle with the given method. * diff --git a/src/org/infinity/util/CreMapCache.java b/src/org/infinity/util/CreMapCache.java index fed7355ee..70deb28f3 100644 --- a/src/org/infinity/util/CreMapCache.java +++ b/src/org/infinity/util/CreMapCache.java @@ -10,7 +10,6 @@ import java.util.Locale; import java.util.Map; import java.util.Set; -import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import org.infinity.NearInfinity; @@ -134,43 +133,43 @@ private static void initialize() { statusBar.setMessage(message); } - ThreadPoolExecutor executor = Misc.createThreadPool(); - List files = ResourceFactory.getResources("CRE"); - // Including CHR resources to reduce number of warnings in IWD/IWD2 if NPC mods are installed - files.addAll(ResourceFactory.getResources("CHR", Profile.getProperty(Profile.Key.GET_GAME_EXTRA_FOLDERS))); - for (final ResourceEntry entry1 : files) { - if (entry1 == null) { - continue; + try (final Threading threadPool = new Threading()) { + List files = ResourceFactory.getResources("CRE"); + // Including CHR resources to reduce number of warnings in IWD/IWD2 if NPC mods are installed + files.addAll(ResourceFactory.getResources("CHR", Profile.getProperty(Profile.Key.GET_GAME_EXTRA_FOLDERS))); + for (final ResourceEntry entry1 : files) { + if (entry1 == null) { + continue; + } + + threadPool.submit(new CreWorker(entry1)); } - Misc.isQueueReady(executor, true, -1); - executor.execute(new CreWorker(entry1)); - } + SCRIPT_NAMES_ARE.add("none"); // default script name for many CRE resources + for (final ResourceEntry entry2 : ResourceFactory.getResources("ARE")) { + if (entry2 == null) { + continue; + } - SCRIPT_NAMES_ARE.add("none"); // default script name for many CRE resources - for (final ResourceEntry entry2 : ResourceFactory.getResources("ARE")) { - if (entry2 == null) { - continue; + threadPool.submit(new AreWorker(entry2)); } - Misc.isQueueReady(executor, true, -1); - executor.execute(new AreWorker(entry2)); - } + for (final ResourceEntry entry3 : ResourceFactory.getResources("INI")) { + if (entry3 == null) { + continue; + } - for (final ResourceEntry entry3 : ResourceFactory.getResources("INI")) { - if (entry3 == null) { - continue; + threadPool.submit(new IniWorker(entry3)); } - Misc.isQueueReady(executor, true, -1); - executor.execute(new IniWorker(entry3)); - } - - executor.shutdown(); - try { - executor.awaitTermination(60, TimeUnit.SECONDS); - } catch (InterruptedException e) { - e.printStackTrace(); + threadPool.shutdown(); + try { + threadPool.awaitTermination(60, TimeUnit.SECONDS); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } catch (Exception e) { + // ignored } if (statusBar != null && statusBar.getMessage().startsWith(message)) { diff --git a/src/org/infinity/util/IdsMap.java b/src/org/infinity/util/IdsMap.java index 179dec5c8..338144beb 100644 --- a/src/org/infinity/util/IdsMap.java +++ b/src/org/infinity/util/IdsMap.java @@ -13,8 +13,6 @@ import java.util.TreeMap; import java.util.TreeSet; -import javax.swing.JOptionPane; - import org.infinity.resource.bcs.ScriptInfo; import org.infinity.resource.bcs.Signatures; import org.infinity.resource.key.ResourceEntry; @@ -26,18 +24,18 @@ public class IdsMap { private final ResourceEntry entry; private final boolean caseSensitive; - public IdsMap(ResourceEntry entry) { + public IdsMap(ResourceEntry entry) throws Exception { this.entry = entry; this.caseSensitive = IdsMapCache.isCaseSensitiveMatch(entry.getResourceName()); - try { +// try { if (entry.getExtension().equalsIgnoreCase("IDS")) { parseIDS(); } else if (entry.getExtension().equalsIgnoreCase("2DA")) { parse2DA(); } - } catch (Exception e) { - e.printStackTrace(); - } +// } catch (Exception e) { +// e.printStackTrace(); +// } } @Override @@ -119,8 +117,7 @@ private void parse2DA() throws Exception { try { extract2DA(token); } catch (NumberFormatException e) { - JOptionPane.showMessageDialog(null, "Error interpreting " + entry + ": " + token, "Error", - JOptionPane.ERROR_MESSAGE); + throw new IllegalArgumentException(String.format("Unexpected token [resource=%s, token=\"%s\"]", entry, token)); } } } @@ -133,8 +130,7 @@ private void parseIDS() throws Exception { try { extractIDS(token); } catch (NumberFormatException e) { - JOptionPane.showMessageDialog(null, "Error interpreting " + entry + ": " + token, "Error", - JOptionPane.ERROR_MESSAGE); + throw new IllegalArgumentException(String.format("Unexpected token [resource=%s, token=\"%s\"]", entry, token)); } } @@ -150,8 +146,7 @@ private void parseIDS() throws Exception { try { extractIDS(token); } catch (NumberFormatException e) { - JOptionPane.showMessageDialog(null, "Error interpreting " + entry + ": " + token, "Error", - JOptionPane.ERROR_MESSAGE); + throw new IllegalArgumentException(String.format("Unexpected token [resource=%s, token=\"%s\"]", entry, token)); } } } diff --git a/src/org/infinity/util/IdsMapCache.java b/src/org/infinity/util/IdsMapCache.java index 0b91c6ebe..2fb87a3d3 100644 --- a/src/org/infinity/util/IdsMapCache.java +++ b/src/org/infinity/util/IdsMapCache.java @@ -7,9 +7,12 @@ import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; import java.util.Locale; import java.util.Map; +import java.util.Set; +import org.infinity.resource.Profile; import org.infinity.resource.ResourceFactory; import org.infinity.resource.bcs.ScriptInfo; import org.infinity.resource.key.ResourceEntry; @@ -18,6 +21,11 @@ public class IdsMapCache { /** Maps upper-cased name of IDS resource to parsed resource. */ private static final Map CACHE = new HashMap<>(); + /** List of IDS resource names that are known to be malformed. */ + private static final Set BLACKLIST = new HashSet<>(); + + private static boolean blackListInitialized = false; + public static void remove(ResourceEntry entry) { if (entry != null) { CACHE.remove(entry.getResourceName().toUpperCase(Locale.ENGLISH)); @@ -26,10 +34,16 @@ public static void remove(ResourceEntry entry) { public static void clearCache() { CACHE.clear(); + blackListInitialized = false; } public static synchronized IdsMap get(String name) { IdsMap retVal = null; + + if (isBlackListed(name)) { + return retVal; + } + if (name != null) { name = name.trim().toUpperCase(Locale.ENGLISH); retVal = CACHE.get(name); @@ -43,8 +57,12 @@ public static synchronized IdsMap get(String name) { } } if (entry != null) { - retVal = new IdsMap(entry); - CACHE.put(name, retVal); + try { + retVal = new IdsMap(entry); + CACHE.put(name, retVal); + } catch (Exception e) { + System.err.println(e.getMessage()); + } } } } @@ -246,4 +264,38 @@ private static String prettifyName(String name) { retVal = Character.toUpperCase(retVal.charAt(0)) + retVal.substring(1); return retVal; } + + /** Returns {@code true} if the specified IDS resref is blacklisted. */ + private static boolean isBlackListed(String name) { + updateBlackList(false); + boolean retVal = true; + if (name != null) { + final String entry = name.trim().toUpperCase(Locale.ROOT); + retVal = BLACKLIST.contains(entry); + } + return retVal; + } + + /** Creates a blacklist of IDS resource names which are known to be malformed. */ + private static void updateBlackList(boolean forced) { + if (forced || !blackListInitialized) { + synchronized (BLACKLIST) { + BLACKLIST.clear(); + switch (Profile.getGame()) { + case PSTEE: + BLACKLIST.add("COLOR.IDS"); + break; + case IWD: + case IWDHoW: + case IWDHowTotLM: + case IWD2: + case IWD2EE: + BLACKLIST.add("PREFAB.IDS"); + break; + default: + } + blackListInitialized = true; + } + } + } } diff --git a/src/org/infinity/util/MassExporter.java b/src/org/infinity/util/MassExporter.java index f790f138c..fd7bf4c8e 100644 --- a/src/org/infinity/util/MassExporter.java +++ b/src/org/infinity/util/MassExporter.java @@ -31,7 +31,7 @@ import java.util.List; import java.util.Locale; import java.util.Set; -import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; import java.util.regex.Pattern; import java.util.regex.PatternSyntaxException; import java.util.stream.Collectors; @@ -75,6 +75,8 @@ import org.infinity.resource.graphics.MosDecoder; import org.infinity.resource.graphics.MosV1Decoder; import org.infinity.resource.graphics.PvrDecoder; +import org.infinity.resource.graphics.TisConvert; +import org.infinity.resource.graphics.TisConvert.Config; import org.infinity.resource.graphics.TisDecoder; import org.infinity.resource.graphics.TisResource; import org.infinity.resource.key.ResourceEntry; @@ -404,40 +406,41 @@ public void run() { // executing multithreaded search boolean isCancelled = false; - ThreadPoolExecutor executor = Misc.createThreadPool(); - progress = new ProgressMonitor(NearInfinity.getInstance(), "Exporting...", - String.format(FMT_PROGRESS, getResourceCount(), getResourceCount()), 0, selectedFiles.size()); - progress.setMillisToDecideToPopup(0); - progress.setMillisToPopup(0); - progress.setProgress(0); - progress.setNote(String.format(FMT_PROGRESS, 0, getResourceCount())); - Debugging.timerReset(); - for (int i = 0, count = getResourceCount(); i < count; i++) { - Misc.isQueueReady(executor, true, -1); - executor.execute(new Worker(selectedFiles.get(i))); - if (progress.isCanceled()) { - isCancelled = true; - break; + try (final Threading threadPool = new Threading()) { + progress = new ProgressMonitor(NearInfinity.getInstance(), "Exporting...", + String.format(FMT_PROGRESS, getResourceCount(), getResourceCount()), 0, selectedFiles.size()); + progress.setMillisToDecideToPopup(0); + progress.setMillisToPopup(0); + progress.setProgress(0); + progress.setNote(String.format(FMT_PROGRESS, 0, getResourceCount())); + Debugging.timerReset(); + for (int i = 0, count = getResourceCount(); i < count; i++) { + threadPool.submit(new Worker(selectedFiles.get(i))); + if (progress.isCanceled()) { + isCancelled = true; + break; + } } - } - - // enforcing thread termination if process has been cancelled - if (isCancelled) { - executor.shutdownNow(); - } else { - executor.shutdown(); - } - // waiting for pending threads to terminate - while (!executor.isTerminated()) { - if (!isCancelled && progress.isCanceled()) { - executor.shutdownNow(); - isCancelled = true; + // enforcing thread termination if process has been cancelled + if (isCancelled) { + threadPool.shutdownNow(); + } else { + threadPool.shutdown(); } - try { - Thread.sleep(1); - } catch (InterruptedException e) { + + // waiting for pending threads to terminate + while (!threadPool.isTerminated()) { + if (!isCancelled && progress.isCanceled()) { + isCancelled = true; + threadPool.shutdownNow(); + } + try { + threadPool.awaitTermination(10L, TimeUnit.MILLISECONDS); + } catch (InterruptedException e) { + } } + } catch (Exception e) { } if (isCancelled) { @@ -629,7 +632,7 @@ private void tisToPng(ResourceEntry entry, Path output) throws Exception { TisDecoder decoder = TisDecoder.loadTis(entry); if (decoder != null) { int tileCount = decoder.getTileCount(); - int columns = TisResource.calcTileWidth(entry, 1); + int columns = TisConvert.calcTilesetWidth(entry, true, 1); int rows = tileCount / columns; if ((tileCount % columns) != 0) { rows++; @@ -722,11 +725,42 @@ private void exportResource(ResourceEntry entry, Path output) throws Exception { boolean isTisV2 = isTis && (info[1] == 0x0c); if (isTis && cbConvertTisVersion.isSelected() && !isTisV2 && cbConvertTisList.getSelectedIndex() == 1) { - TisResource tis = new TisResource(entry); - tis.convertToPvrzTis(TisResource.makeTisFileNameValid(output), false); + final Path tisFile = TisConvert.makeTisFileNameValid(output); + final TisResource tis = new TisResource(entry); + final ResourceEntry wedEntry = TisConvert.findWed(entry, true); + final int tilesPerRow = TisConvert.calcTilesetWidth(wedEntry, false, tis.getDecoder().getTileCount()); + final int pvrzBaseIndex = TisConvert.calcPvrzBaseIndex(tisFile); + final TisConvert.OverlayConversion convert = (Profile.getEngine() == Profile.Engine.BG2) + ? TisConvert.OverlayConversion.BG2_TO_BG2EE + : TisConvert.OverlayConversion.NONE; + final TisConvert.Config config = Config.createConfigPvrz(tisFile, tis.getDecoder(), wedEntry, tilesPerRow, -1, + TisConvert.Config.MAX_TEXTURE_SIZE, pvrzBaseIndex, TisConvert.Config.DEFAULT_BORDER_SIZE, + TisConvert.Config.MAX_TEXTURE_SIZE / 2, true, true, convert); + TisConvert.convertToPvrzTis(config, false, null); } else if (isTis && cbConvertTisVersion.isSelected() && isTisV2 && cbConvertTisList.getSelectedIndex() == 0) { TisResource tis = new TisResource(entry); - tis.convertToPaletteTis(output, false); + + // overlay conversion mode depends on game and WED overlay movement type + final ResourceEntry wedEntry = TisConvert.findWed(entry, true); + final int movementType = TisConvert.getTisMovementType(wedEntry, false); + final TisConvert.OverlayConversion convert; + switch (Profile.getGame()) { + case BG2EE: + case IWDEE: + case PSTEE: + case EET: + convert = (movementType == 0) ? TisConvert.OverlayConversion.BG2EE_TO_BG2 : TisConvert.OverlayConversion.NONE; + break; + case BG1EE: + convert = (movementType == 2) ? TisConvert.OverlayConversion.BG2EE_TO_BG2 : TisConvert.OverlayConversion.NONE; + break; + default: + convert = TisConvert.OverlayConversion.NONE; + } + + final TisConvert.Config config = Config.createConfigPalette(output, tis.getTileList(), tis.getDecoder(), + wedEntry, convert); + TisConvert.convertToPaletteTis(config, false, null); } else if (size >= 0) { try (InputStream is = entry.getResourceDataAsStream()) { // Keep trying. File may be in use by another thread. diff --git a/src/org/infinity/util/Misc.java b/src/org/infinity/util/Misc.java index b70721ed3..cd616a08d 100644 --- a/src/org/infinity/util/Misc.java +++ b/src/org/infinity/util/Misc.java @@ -12,9 +12,6 @@ import java.lang.reflect.Constructor; import java.nio.charset.Charset; import java.util.Comparator; -import java.util.concurrent.ArrayBlockingQueue; -import java.util.concurrent.ThreadPoolExecutor; -import java.util.concurrent.TimeUnit; import java.util.prefs.Preferences; import javax.swing.JComponent; @@ -300,61 +297,6 @@ public static final long signExtend(long value, int bits) { return (value << (64 - bits)) >> (64 - bits); } - /** - * Creates a thread pool with a pool size depending on the number of available CPU cores.
- *
- * numThreads: Number of available CPU cores.
- * maxQueueSize: 2 x {@code numThreads}.
- * - * @return A ThreadPoolExecutor instance. - */ - public static ThreadPoolExecutor createThreadPool() { - int numThreads = Runtime.getRuntime().availableProcessors(); - return createThreadPool(numThreads, numThreads * 2); - } - - /** - * Creates a thread pool with the specified parameters. - * - * @param numThreads Max. number of parallel threads to execute. Must be >= 1. - * @param maxQueueSize Max. size of the working queue. Must be >= {@code numThreads}. - * @return A ThreadPoolExecutor instance. - */ - public static ThreadPoolExecutor createThreadPool(int numThreads, int maxQueueSize) { - numThreads = Math.max(1, numThreads); - maxQueueSize = Math.max(numThreads, maxQueueSize); - return new ThreadPoolExecutor(numThreads, numThreads, 0L, TimeUnit.MILLISECONDS, - new ArrayBlockingQueue(maxQueueSize)); - } - - /** - * Helper routine which can be used to check or block execution of new threads while the blocking queue is full. - * - * @param executor The executor to query. - * @param block Specify {@code true} to block execution as long as the queue is full. - * @param maxWaitMs Specify max. time to block queue, in milliseconds. Specify -1 to block indefinitely. - * @return {@code true} if queue is ready for new elements, {@code false} otherwise. - */ - public static boolean isQueueReady(ThreadPoolExecutor executor, boolean block, int maxWaitMs) { - if (executor != null) { - if (block) { - if (maxWaitMs < 0) { - maxWaitMs = Integer.MAX_VALUE; - } - int curWaitMs = 0; - while (curWaitMs < maxWaitMs && executor.getQueue().size() > executor.getCorePoolSize()) { - try { - Thread.sleep(1); - } catch (InterruptedException e) { - } - curWaitMs++; - } - } - return executor.getQueue().size() <= executor.getCorePoolSize(); - } - return false; - } - /** * Returns a prototype dimension object based on the height of {@code c} and the width of (@code prototype}. * diff --git a/src/org/infinity/util/ResourceStructure.java b/src/org/infinity/util/ResourceStructure.java index ed5719af5..7431ed94f 100644 --- a/src/org/infinity/util/ResourceStructure.java +++ b/src/org/infinity/util/ResourceStructure.java @@ -114,19 +114,29 @@ public int remove(int offset) { return removeItem(offset); } + /** Removes all items from the structure. */ public void clear() { list.clear(); cursize = 0; } + /** Returns the current structure size, in bytes. */ public int size() { return cursize; } + /** Returns {@code true} if the structure is empty. */ public boolean isEmpty() { return list.isEmpty(); } + /** + * Returns the {@link Item} instance found at the specified offset. + * + * @param offset Absolute byte offset in the structure. + * @return {@link Item} instance at the specified offset if found. + * @throws IndexOutOfBoundsException if the specified offset is invalid. + */ public Item get(int offset) { return getItem(offset); } diff --git a/src/org/infinity/util/Threading.java b/src/org/infinity/util/Threading.java new file mode 100644 index 000000000..0ab00dc1c --- /dev/null +++ b/src/org/infinity/util/Threading.java @@ -0,0 +1,483 @@ +// Near Infinity - An Infinity Engine Browser and Editor +// Copyright (C) 2001 Jon Olav Hauglid +// See LICENSE.txt for license information + +package org.infinity.util; + +import java.util.Collection; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.Callable; +import java.util.concurrent.Executors; +import java.util.concurrent.ForkJoinTask; +import java.util.concurrent.Future; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * A convenience class for performing multiple tasks in parallel. + */ +public class Threading implements AutoCloseable { + /** Controls the amount of threads to allocate by a new thread pool. */ + public enum Priority { + /** Always allocates a single thread even if more are available. */ + LOWEST(0.0), + /** Allocates about 25 percent of available free threads, but always at least one thread. */ + LOW(0.25), + /** Allocates about 50 percent of available free threads, but always at least one thread. */ + NORMAL(0.5), + /** Allocates about 75 percent of available free threads, but always at least one thread. */ + HIGH(0.75), + /** Allocates all available free threads, but always at least one thread. */ + HIGHEST(1.0), + + /** Always allocates about 25 percent of {@link Threading#MAX_THREADS_AVAILABLE} threads. */ + ABS_LOW(-0.25), + /** Always allocates about 50 percent of {@link Threading#MAX_THREADS_AVAILABLE} threads. */ + ABS_NORMAL(-0.5), + /** Always allocates about 75 percent of {@link Threading#MAX_THREADS_AVAILABLE} threads. */ + ABS_HIGH(-0.75), + /** Always allocates all of {@link Threading#MAX_THREADS_AVAILABLE} threads. */ + ABS_HIGHEST(-1.0), + ; + + private final double factor; + + private Priority(double factor) { + this.factor = Math.max(-1.0, Math.min(1.0, factor)); + } + + /** + * Returns the thread allocation factor.
+ * A positive value refers to the currently available number of free threads.
+ * A negative value refers to the max. number of available threads {@link Threading#MAX_THREADS_AVAILABLE}. + */ + public double getFactor() { + return factor; + } + + @Override + public String toString() { + return this.name() + " [factor=" + this.factor + "]"; + } + } + + /** Defines the total number of threads that can be executed in parallel on the current system. */ + public static final int MAX_THREADS_AVAILABLE = Runtime.getRuntime().availableProcessors(); + + /** Contains the number of remaining threads for use by new thread pools. */ + private static AtomicInteger THREADS_AVAILABLE = new AtomicInteger(MAX_THREADS_AVAILABLE); + + + /** Stores submitted tasks for internal evaluation purposes. */ + private final List> taskList = new LinkedList<>(); + + private final ThreadPoolExecutor executor; + private final int numThreads; + + private boolean closed; + private Object initialized; + private Object released; + + /** + * Initializes a new {@link Threading} object with {@link Priority#NORMAL}. + */ + public Threading() { + this(Priority.NORMAL); + } + + /** + * Initializes a new {@link Threading} object. + * + * @param priority {@link Priority} value that is used to calculate the optimal number of threads for this thread + * pool. + */ + public Threading(Priority priority) { + this(calculateThreadCount(priority)); + } + + /** + * Initializes a new {@link Threading} object. + * + * @param numThreads Max. number of active threads. + * @throws IllegalArgumentException if numThreads <= 0. + */ + private Threading(int numThreads) { + this.numThreads = getValidatedThreadCount(numThreads); + this.executor = (ThreadPoolExecutor) Executors.newFixedThreadPool(getThreadCount()); + allocateThreads(); + } + + /** + * Returns the number of threads reserved by this thread pool. + * + * @return number of threads reserved by this thread pool. + */ + public int getThreadCount() { + return numThreads; + } + + /** + * Returns {@code true} if the task list contains one or more entries. + * + * @param forceUpdate Specify {@code true} to remove all completed or cancelled tasks before calculating the result. + * @return {@code true} if the task list contains one or more tasks that have not yet been completed or cancelled, + * {@code false} otherwise. + */ + public boolean hasTasks(boolean forceUpdate) { + if (forceUpdate) { + dispose(); + } + return !taskList.isEmpty(); + } + + /** + * Returns the approximate number of threads that are actively executing tasks. + * + * @return the number of threads. + */ + public int getActiveThreadCount() { + return executor.getActiveCount(); + } + + /** + * Returns {@code true} if there are any tasks submitted to this thread pool that have not yet begun executing. + * + * @return {@code true} if there are any queued submissions. + */ + public boolean hasQueuedSubmissions() { + return !executor.getQueue().isEmpty(); + } + + /** + * Returns an estimate of the number of tasks submitted to this pool that have not yet begun executing. + * + * @return the number of queued submissions. + */ + public int getQueuedSubmissionCount() { + return executor.getQueue().size(); + } + + /** + * Returns {@code true} if this pool has been shut down. + * + * @return {@code true} if this pool has been shut down. + */ + public boolean isShutdown() { + return executor.isShutdown(); + } + + /** + * Returns {@code true} if all tasks have completed following shut down. + * + * @return {@code true} if all tasks have completed following shut down. + */ + public boolean isTerminated() { + return executor.isTerminated(); + } + + /** + * This method removes all completed or cancelled tasks from the task list. + * + * @return Number of remaining {@link ForkJoinTask} instances that haven been completed or cancelled yet. + */ + public synchronized int dispose() { + for (final Iterator> iter = taskList.iterator(); iter.hasNext(); ) { + final Future future = iter.next(); + if (future.isDone()) { + iter.remove(); + } + } + return taskList.size(); + } + + /** + * Possibly initiates an orderly shutdown in which previously submitted tasks are executed, but no new tasks will be + * accepted. Tasks that are in the process of being submitted concurrently during the course of this method may or may + * not be rejected. + * + *

+ * The predefined {@code Threading} objects {@link #DEFAULT} and {@link #SINGLE} cannot be terminated. + *

+ */ + public void shutdown() { + executor.shutdown(); + releaseThreads(); + } + + /** + * Possibly attempts to cancel and/or stop all tasks, and reject all subsequently submitted tasks. Invocation has no + * additional effect if already shut down. Otherwise, tasks that are in the process of being submitted or executed + * concurrently during the course of this method may or may not be rejected. This method cancels both existing and + * unexecuted tasks, in order to permit termination in the presence of task dependencies. + * + *

+ * The predefined {@code Threading} objects {@link #DEFAULT} and {@link #SINGLE} cannot be terminated. + *

+ */ + public void shutdownNow() { + executor.shutdownNow(); + releaseThreads(); + dispose(); + } + + /** + * Submits a new task to be executed by the thread pool and returns a {@code Future} representing that task. The + * Future's {@code get} method will return the given result upon successful completion. + * + * @param Type of the task's result. + * @param task The task to submit. + * @return a {@link Future} representing pending completion of the task. + * @throws RejectedExecutionException if the task cannot bescheduled for execution. + * @throws NullPointerException if the task is {@code null}. + */ + public Future submit(Callable task) { + Objects.requireNonNull(task, "Task is null"); + return registerFuture(executor.submit(task)); + } + + /** + * Submits a {@code Runnable} task for execution in the thread pool and returns a {@code Future} representing that + * task. The Future's {@code get} method will return the given result upon successful completion. + * + * @param Type of the result. + * @param task The task to submit, as {@link Runnable} object. + * @param result The result to return. + * @return a {@link Future} representing pending completion of the task. + * @throws RejectedExecutionException if the task cannot bescheduled for execution. + * @throws NullPointerException if the task is {@code null}. + */ + public Future submit(Runnable task, T result) { + Objects.requireNonNull(task, "Task is null"); + return registerFuture(executor.submit(task, result)); + } + + /** + * Submits a {@code Runnable} task for execution in the thread pool and returns a {@code Future} representing that + * task. The Future's {@code get} method will return {@code null} upon successful completion. + * + * @param task The task to submit, as {@link Runnable} object. + * @return a {@link Future} representing pending completion of the task. + * @throws RejectedExecutionException if the task cannot bescheduled for execution. + * @throws NullPointerException if the task is {@code null}. + */ + public Future submit(Runnable task) { + Objects.requireNonNull(task, "Task is null"); + return registerFuture(executor.submit(task)); + } + + public List> invokeAll(Collection> tasks) throws InterruptedException { + Objects.requireNonNull(tasks, "Tasks collection is null"); + + final List> retVal = executor.invokeAll(tasks); + if (retVal != null) { + for (final Iterator> iter = retVal.iterator(); iter.hasNext(); ) { + final Future future = iter.next(); + registerFuture(future); + } + } + return retVal; + } + + /** + * Blocks until all submitted tasks have completed execution, or the current thread is interrupted, whichever happens + * first. Unlike {@link #awaitTermination(long, TimeUnit)} this method does not depend on a shutdown request, which + * allows to submit more tasks after completion. + * + * @param timeout the maximum time to wait. + * @param unit the time unit of the timeout argument. + * @return {@code true} if all submitted tasks terminated and {@code false} if the timeout elapsed before termination. + * @throws InterruptedException if interrupted while waiting. + */ + public void waitForCompletion() throws InterruptedException { + waitForCompletion(Long.MAX_VALUE, TimeUnit.NANOSECONDS); + } + + /** + * Blocks until all submitted tasks have completed execution, or the timeout occurs, or the current thread is + * interrupted, whichever happens first. Unlike {@link #awaitTermination(long, TimeUnit)} this method does not + * depend on a shutdown request, which allows to submit more tasks after completion. + * + * @param timeout the maximum time to wait. + * @param unit the time unit of the timeout argument. + * @return {@code true} if all submitted tasks terminated and {@code false} if the timeout elapsed before termination. + * @throws InterruptedException if interrupted while waiting. + */ + public boolean waitForCompletion(long timeout, TimeUnit unit) throws InterruptedException { + boolean retVal = !hasTasks(true); + + long nsTimeOut = unit.toNanos(timeout); + long timeBase, timeElapsed; + timeBase = System.nanoTime(); + while (!retVal && nsTimeOut > 0L) { + retVal = !hasTasks(true); + timeElapsed = System.nanoTime() - timeBase; + nsTimeOut -= timeElapsed; + + if (nsTimeOut > 0L) { + timeBase = System.nanoTime(); + Thread.sleep((nsTimeOut > 500_000L) ? 1L : 0L); + timeElapsed = System.nanoTime() - timeBase; + nsTimeOut -= timeElapsed; + } + + timeBase = System.nanoTime(); + } + + return retVal; + } + + /** + * Blocks until all tasks have completed execution after a shutdown request, or the current thread is interrupted, + * whichever happens first. + * + * @throws InterruptedException if interrupted while waiting. + */ + public void awaitTermination() throws InterruptedException { + awaitTermination(Long.MAX_VALUE, TimeUnit.NANOSECONDS); + } + + /** + * Blocks until all tasks have completed execution after a shutdown request, or the timeout occurs, or the current + * thread is interrupted, whichever happens first. + * + * @param timeout the maximum time to wait. + * @param unit the time unit of the timeout argument. + * @return {@code true} if this thread pool terminated and {@code false} if the timeout elapsed before termination. + * @throws InterruptedException if interrupted while waiting. + */ + public boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException { + return executor.awaitTermination(timeout, unit); + } + + /** + * Performs an orderly shutdown in which previously submitted tasks are executed, but no new tasks will be accepted. + * Tasks that are in the process of being submitted concurrently during the course of this method may or may not be + * rejected. + */ + @Override + public void close() throws Exception { + // initiating orderly shutdown + shutdown(); + + if (!closed) { + closed = true; + + // waiting for all tasks to be done, or a timeout occurred + final Thread cleanupThread = new Thread(() -> { + long timeOutNs = TimeUnit.SECONDS.toNanos(60L); + long timeBase = System.nanoTime(); + while (hasTasks(true) && timeOutNs > 0L) { + try { + Thread.sleep(100L); + } catch (InterruptedException e) { + } + final long timeElapsed = System.nanoTime() - timeBase; + timeBase = System.nanoTime(); + timeOutNs -= timeElapsed; + } + }); + cleanupThread.setPriority(Thread.MIN_PRIORITY); + cleanupThread.start(); + } + } + + @Override + public int hashCode() { + return executor.hashCode(); + } + + @Override + public boolean equals(Object obj) { + return executor.equals(obj); + } + + @Override + public String toString() { + return "Threading [numThreads=" + numThreads + ", executor=" + executor + "]"; + } + + /** + * Used internally to register new {@code Future} objects. + * + * @param Type of the task's result. + * @param future {@link ForkJoinTask} object to register. + * @return the specified {@code future} argument. + */ + private > U registerFuture(U future) { + Objects.requireNonNull(future, "Future is null"); + taskList.add(future); + return future; + } + + /** + * Reserves the amount of threads defined by the given {@code Threading} object and updates + * {@link #THREADS_AVAILABLE}. + * + * @param threading {@link Threading} object that requests thread reservation. + * @return {@code true} if reservation completed successfully, {@code false} otherwise. + */ + private synchronized boolean allocateThreads() { + if (initialized == null && released == null) { + THREADS_AVAILABLE.set(Math.max(1, THREADS_AVAILABLE.get() - numThreads)); + initialized = new Object(); + return true; + } + return false; + } + + /** + * Releases the amount of threads defined by the given {@code Threading} object and updates + * {@link #THREADS_AVAILABLE}. + * + * @param threading {@link Threading} object that requests the release of threads. + * @return {@code true} if release complete successfully, {@code false} otherwise. + */ + private synchronized boolean releaseThreads() { + if (initialized != null && released == null) { + THREADS_AVAILABLE.set(Math.min(MAX_THREADS_AVAILABLE, THREADS_AVAILABLE.get() + numThreads)); + released = new Object(); + return true; + } + return false; + } + + /** + * Calculates the number of threads to reserve based on the given {@code priority}. + * + * @param priority the thread {@link Priority}. Default: {@link Priority#NORMAL} + * @return Number of threads to reserve according to the given {@code priority}. + */ + public static int calculateThreadCount(Priority priority) { + if (priority == null) { + priority = Priority.NORMAL; + } + + final int numThreadsTotal = (priority.getFactor() < 0) ? MAX_THREADS_AVAILABLE : THREADS_AVAILABLE.get(); + final int numThreadsCalculated = (int) Math.floor(Math.abs(priority.getFactor()) * numThreadsTotal); + return Math.max(1, Math.min(MAX_THREADS_AVAILABLE, numThreadsCalculated)); + } + + /** + * Returns the number of currently available threads that can be allocated by new {@link Threading} objects. + * + * @return number of currently available threads. + */ + public static int getAvailableThreads() { + return THREADS_AVAILABLE.get(); + } + + /** + * Ensures that the returned thread count is always within the range of [1, # available processors]. + * + * @param numThreads Thread count to validate. + * @return The validated thread count. + */ + private static int getValidatedThreadCount(int numThreads) { + return Math.max(1, Math.min(MAX_THREADS_AVAILABLE, numThreads)); + } +} diff --git a/src/org/infinity/util/io/FileWatcher.java b/src/org/infinity/util/io/FileWatcher.java index 3cd405097..2a7b3b14d 100644 --- a/src/org/infinity/util/io/FileWatcher.java +++ b/src/org/infinity/util/io/FileWatcher.java @@ -21,6 +21,7 @@ import java.util.EventObject; import java.util.HashMap; import java.util.Iterator; +import java.util.Objects; import java.util.concurrent.TimeUnit; /** @@ -57,6 +58,7 @@ protected FileWatcher(long timeOutMS) { this.watcher = FileSystems.getDefault().newWatchService(); } catch (IOException e) { this.watcher = null; + System.err.println("Could not initialize file watcher: " + e.getMessage()); } this.thread = null; this.timeOutMS = Math.max(timeOutMS, 0L); @@ -95,6 +97,10 @@ public FileWatchListener[] getFileWatchListeners() { /** Starts the file watcher background process. Does nothing if the process has already started. */ public boolean start() { + if (Objects.isNull(watcher)) { + return false; + } + if (thread == null) { thread = new Thread(this); thread.start(); @@ -163,6 +169,10 @@ public void register(Path dir, boolean recursive) { * @param notifyModify Whether to notify if a file has been modified in the directory. */ public void register(Path dir, boolean recursive, boolean notifyCreate, boolean notifyDelete, boolean notifyModify) { + if (Objects.isNull(watcher)) { + return; + } + dir = FileManager.resolve(dir); if (dir != null && FileEx.create(dir).isDirectory()) { if (recursive) {