diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..e67c05d
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,245 @@
+# Remove the line below if you want to inherit .editorconfig settings from higher directories
+root = true
+
+# C# files
+[*.cs]
+
+#### Core EditorConfig Options ####
+
+# Indentation and spacing
+indent_size = 4
+indent_style = space
+tab_width = 4
+
+# New line preferences
+end_of_line = crlf
+insert_final_newline = false
+
+#### .NET Coding Conventions ####
+
+# Organize usings
+dotnet_separate_import_directive_groups = true
+dotnet_sort_system_directives_first = true
+file_header_template = unset
+
+# this. and Me. preferences
+dotnet_style_qualification_for_event = false:suggestion
+dotnet_style_qualification_for_field = false
+dotnet_style_qualification_for_method = false:suggestion
+dotnet_style_qualification_for_property = false:suggestion
+
+# Language keywords vs BCL types preferences
+dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion
+dotnet_style_predefined_type_for_member_access = false:suggestion
+
+# Parentheses preferences
+dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:suggestion
+dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:suggestion
+dotnet_style_parentheses_in_other_operators = never_if_unnecessary:suggestion
+dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:suggestion
+
+# Modifier preferences
+dotnet_style_require_accessibility_modifiers = for_non_interface_members
+
+# Expression-level preferences
+dotnet_style_coalesce_expression = true
+dotnet_style_collection_initializer = true
+dotnet_style_explicit_tuple_names = true
+dotnet_style_namespace_match_folder = true
+dotnet_style_null_propagation = true
+dotnet_style_object_initializer = true
+dotnet_style_operator_placement_when_wrapping = beginning_of_line
+dotnet_style_prefer_auto_properties = true:suggestion
+dotnet_style_prefer_collection_expression = when_types_loosely_match
+dotnet_style_prefer_compound_assignment = true
+dotnet_style_prefer_conditional_expression_over_assignment = true:suggestion
+dotnet_style_prefer_conditional_expression_over_return = true:suggestion
+dotnet_style_prefer_foreach_explicit_cast_in_source = when_strongly_typed
+dotnet_style_prefer_inferred_anonymous_type_member_names = true
+dotnet_style_prefer_inferred_tuple_names = true
+dotnet_style_prefer_is_null_check_over_reference_equality_method = true
+dotnet_style_prefer_simplified_boolean_expressions = true
+dotnet_style_prefer_simplified_interpolation = true
+
+# Field preferences
+dotnet_style_readonly_field = true
+
+# Parameter preferences
+dotnet_code_quality_unused_parameters = all
+
+# Suppression preferences
+dotnet_remove_unnecessary_suppression_exclusions = none
+
+# New line preferences
+dotnet_style_allow_multiple_blank_lines_experimental = false:suggestion
+dotnet_style_allow_statement_immediately_after_block_experimental = true:suggestion
+
+#### C# Coding Conventions ####
+
+# var preferences
+csharp_style_var_elsewhere = true:suggestion
+csharp_style_var_for_built_in_types = true:suggestion
+csharp_style_var_when_type_is_apparent = true:suggestion
+
+# Expression-bodied members
+csharp_style_expression_bodied_accessors = false:suggestion
+csharp_style_expression_bodied_constructors = false:suggestion
+csharp_style_expression_bodied_indexers = false:suggestion
+csharp_style_expression_bodied_lambdas = when_on_single_line:suggestion
+csharp_style_expression_bodied_local_functions = false:suggestion
+csharp_style_expression_bodied_methods = false:suggestion
+csharp_style_expression_bodied_operators = false:suggestion
+csharp_style_expression_bodied_properties = false:suggestion
+
+# Pattern matching preferences
+csharp_style_pattern_matching_over_as_with_null_check = true
+csharp_style_pattern_matching_over_is_with_cast_check = true
+csharp_style_prefer_extended_property_pattern = true
+csharp_style_prefer_not_pattern = true
+csharp_style_prefer_pattern_matching = true:suggestion
+csharp_style_prefer_switch_expression = false:error
+
+# Null-checking preferences
+csharp_style_conditional_delegate_call = true
+
+# Modifier preferences
+csharp_prefer_static_anonymous_function = true
+csharp_prefer_static_local_function = true
+csharp_preferred_modifier_order = public,private,protected,internal,file,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,required,volatile,async
+csharp_style_prefer_readonly_struct = true
+csharp_style_prefer_readonly_struct_member = true
+
+# Code-block preferences
+csharp_prefer_braces = true:suggestion
+csharp_prefer_simple_using_statement = true:warning
+csharp_style_namespace_declarations = file_scoped:warning
+csharp_style_prefer_method_group_conversion = true:suggestion
+csharp_style_prefer_primary_constructors = true
+csharp_style_prefer_top_level_statements = false:error
+
+# Expression-level preferences
+csharp_prefer_simple_default_expression = true
+csharp_style_deconstructed_variable_declaration = true
+csharp_style_implicit_object_creation_when_type_is_apparent = true
+csharp_style_inlined_variable_declaration = true
+csharp_style_prefer_index_operator = true
+csharp_style_prefer_local_over_anonymous_function = true:warning
+csharp_style_prefer_null_check_over_type_check = true
+csharp_style_prefer_range_operator = true
+csharp_style_prefer_tuple_swap = true
+csharp_style_prefer_utf8_string_literals = true
+csharp_style_throw_expression = true
+csharp_style_unused_value_assignment_preference = discard_variable
+csharp_style_unused_value_expression_statement_preference = discard_variable:suggestion
+
+# 'using' directive preferences
+csharp_using_directive_placement = inside_namespace:suggestion
+
+# New line preferences
+csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = false:suggestion
+csharp_style_allow_blank_line_after_token_in_arrow_expression_clause_experimental = true
+csharp_style_allow_blank_line_after_token_in_conditional_expression_experimental = false:suggestion
+csharp_style_allow_blank_lines_between_consecutive_braces_experimental = false:suggestion
+csharp_style_allow_embedded_statements_on_same_line_experimental = false:suggestion
+
+#### C# Formatting Rules ####
+
+# New line preferences
+csharp_new_line_before_catch = true
+csharp_new_line_before_else = true
+csharp_new_line_before_finally = true
+csharp_new_line_before_members_in_anonymous_types = true
+csharp_new_line_before_members_in_object_initializers = true
+csharp_new_line_before_open_brace = all
+csharp_new_line_between_query_expression_clauses = true
+
+# Indentation preferences
+csharp_indent_block_contents = true
+csharp_indent_braces = false
+csharp_indent_case_contents = true
+csharp_indent_case_contents_when_block = true
+csharp_indent_labels = one_less_than_current
+csharp_indent_switch_labels = true
+
+# Space preferences
+csharp_space_after_cast = false
+csharp_space_after_colon_in_inheritance_clause = true
+csharp_space_after_comma = true
+csharp_space_after_dot = false
+csharp_space_after_keywords_in_control_flow_statements = true
+csharp_space_after_semicolon_in_for_statement = true
+csharp_space_around_binary_operators = before_and_after
+csharp_space_around_declaration_statements = false
+csharp_space_before_colon_in_inheritance_clause = true
+csharp_space_before_comma = false
+csharp_space_before_dot = false
+csharp_space_before_open_square_brackets = false
+csharp_space_before_semicolon_in_for_statement = false
+csharp_space_between_empty_square_brackets = false
+csharp_space_between_method_call_empty_parameter_list_parentheses = false
+csharp_space_between_method_call_name_and_opening_parenthesis = false
+csharp_space_between_method_call_parameter_list_parentheses = false
+csharp_space_between_method_declaration_empty_parameter_list_parentheses = false
+csharp_space_between_method_declaration_name_and_open_parenthesis = false
+csharp_space_between_method_declaration_parameter_list_parentheses = false
+csharp_space_between_parentheses = false
+csharp_space_between_square_brackets = false
+
+# Wrapping preferences
+csharp_preserve_single_line_blocks = true
+csharp_preserve_single_line_statements = true
+
+#### Naming styles ####
+
+# Naming rules
+
+dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion
+dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface
+dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i
+
+dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion
+dotnet_naming_rule.types_should_be_pascal_case.symbols = types
+dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case
+
+dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion
+dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members
+dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case
+
+dotnet_naming_rule.private_or_internal_field_should_be_endswithunderscore.severity = suggestion
+dotnet_naming_rule.private_or_internal_field_should_be_endswithunderscore.symbols = private_or_internal_field
+dotnet_naming_rule.private_or_internal_field_should_be_endswithunderscore.style = endswithunderscore
+
+# Symbol specifications
+
+dotnet_naming_symbols.interface.applicable_kinds = interface
+dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
+dotnet_naming_symbols.interface.required_modifiers =
+
+dotnet_naming_symbols.private_or_internal_field.applicable_kinds = field
+dotnet_naming_symbols.private_or_internal_field.applicable_accessibilities = internal, private, private_protected
+dotnet_naming_symbols.private_or_internal_field.required_modifiers =
+
+dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum
+dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
+dotnet_naming_symbols.types.required_modifiers =
+
+dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method
+dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
+dotnet_naming_symbols.non_field_members.required_modifiers =
+
+# Naming styles
+
+dotnet_naming_style.pascal_case.required_prefix =
+dotnet_naming_style.pascal_case.required_suffix =
+dotnet_naming_style.pascal_case.word_separator =
+dotnet_naming_style.pascal_case.capitalization = pascal_case
+
+dotnet_naming_style.begins_with_i.required_prefix = I
+dotnet_naming_style.begins_with_i.required_suffix =
+dotnet_naming_style.begins_with_i.word_separator =
+dotnet_naming_style.begins_with_i.capitalization = pascal_case
+
+dotnet_naming_style.endswithunderscore.required_prefix =
+dotnet_naming_style.endswithunderscore.required_suffix = _
+dotnet_naming_style.endswithunderscore.word_separator =
+dotnet_naming_style.endswithunderscore.capitalization = camel_case
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..2040fbc
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,501 @@
+
+# Created by https://www.gitignore.io/api/csharp,visualstudio
+# Edit at https://www.gitignore.io/?templates=csharp,visualstudio
+
+### Csharp ###
+## Ignore Visual Studio temporary files, build results, and
+## files generated by popular Visual Studio add-ons.
+##
+## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
+
+# User-specific files
+*.rsuser
+*.suo
+*.user
+*.userosscache
+*.sln.docstates
+
+# User-specific files (MonoDevelop/Xamarin Studio)
+*.userprefs
+
+# Build results
+[Dd]ebug/
+[Dd]ebugPublic/
+[Rr]elease/
+[Rr]eleases/
+x64/
+x86/
+bld/
+[Bb]in/
+[Oo]bj/
+[Ll]og/
+
+# Visual Studio 2015/2017 cache/options directory
+.vs/
+# Uncomment if you have tasks that create the project's static files in wwwroot
+#wwwroot/
+
+# Visual Studio 2017 auto generated files
+Generated\ Files/
+
+# MSTest test Results
+[Tt]est[Rr]esult*/
+[Bb]uild[Ll]og.*
+
+# NUNIT
+*.VisualState.xml
+TestResult.xml
+
+# Build Results of an ATL Project
+[Dd]ebugPS/
+[Rr]eleasePS/
+dlldata.c
+
+# Benchmark Results
+BenchmarkDotNet.Artifacts/
+
+# .NET Core
+project.lock.json
+project.fragment.lock.json
+artifacts/
+
+# StyleCop
+StyleCopReport.xml
+
+# Files built by Visual Studio
+*_i.c
+*_p.c
+*_h.h
+*.ilk
+*.meta
+*.obj
+*.iobj
+*.pch
+*.pdb
+*.ipdb
+*.pgc
+*.pgd
+*.rsp
+*.sbr
+*.tlb
+*.tli
+*.tlh
+*.tmp
+*.tmp_proj
+*_wpftmp.csproj
+*.log
+*.vspscc
+*.vssscc
+.builds
+*.pidb
+*.svclog
+*.scc
+
+# Chutzpah Test files
+_Chutzpah*
+
+# Visual C++ cache files
+ipch/
+*.aps
+*.ncb
+*.opendb
+*.opensdf
+*.sdf
+*.cachefile
+*.VC.db
+*.VC.VC.opendb
+
+# Visual Studio profiler
+*.psess
+*.vsp
+*.vspx
+*.sap
+
+# Visual Studio Trace Files
+*.e2e
+
+# TFS 2012 Local Workspace
+$tf/
+
+# Guidance Automation Toolkit
+*.gpState
+
+# ReSharper is a .NET coding add-in
+_ReSharper*/
+*.[Rr]e[Ss]harper
+*.DotSettings.user
+
+# JustCode is a .NET coding add-in
+.JustCode
+
+# TeamCity is a build add-in
+_TeamCity*
+
+# DotCover is a Code Coverage Tool
+*.dotCover
+
+# AxoCover is a Code Coverage Tool
+.axoCover/*
+!.axoCover/settings.json
+
+# Visual Studio code coverage results
+*.coverage
+*.coveragexml
+
+# NCrunch
+_NCrunch_*
+.*crunch*.local.xml
+nCrunchTemp_*
+
+# MightyMoose
+*.mm.*
+AutoTest.Net/
+
+# Web workbench (sass)
+.sass-cache/
+
+# Installshield output folder
+[Ee]xpress/
+
+# DocProject is a documentation generator add-in
+DocProject/buildhelp/
+DocProject/Help/*.HxT
+DocProject/Help/*.HxC
+DocProject/Help/*.hhc
+DocProject/Help/*.hhk
+DocProject/Help/*.hhp
+DocProject/Help/Html2
+DocProject/Help/html
+
+# Click-Once directory
+publish/
+
+# Publish Web Output
+*.[Pp]ublish.xml
+*.azurePubxml
+# Note: Comment the next line if you want to checkin your web deploy settings,
+# but database connection strings (with potential passwords) will be unencrypted
+*.pubxml
+*.publishproj
+
+# Microsoft Azure Web App publish settings. Comment the next line if you want to
+# checkin your Azure Web App publish settings, but sensitive information contained
+# in these scripts will be unencrypted
+PublishScripts/
+
+# NuGet Packages
+*.nupkg
+# The packages folder can be ignored because of Package Restore
+**/[Pp]ackages/*
+# except build/, which is used as an MSBuild target.
+!**/[Pp]ackages/build/
+# Uncomment if necessary however generally it will be regenerated when needed
+#!**/[Pp]ackages/repositories.config
+# NuGet v3's project.json files produces more ignorable files
+*.nuget.props
+*.nuget.targets
+
+# Microsoft Azure Build Output
+csx/
+*.build.csdef
+
+# Microsoft Azure Emulator
+ecf/
+rcf/
+
+# Windows Store app package directories and files
+AppPackages/
+BundleArtifacts/
+Package.StoreAssociation.xml
+_pkginfo.txt
+*.appx
+
+# Visual Studio cache files
+# files ending in .cache can be ignored
+*.[Cc]ache
+# but keep track of directories ending in .cache
+!*.[Cc]ache/
+
+# Others
+ClientBin/
+~$*
+*~
+*.dbmdl
+*.dbproj.schemaview
+*.jfm
+*.pfx
+*.publishsettings
+orleans.codegen.cs
+
+# Including strong name files can present a security risk
+# (https://github.com/github/gitignore/pull/2483#issue-259490424)
+#*.snk
+
+# Since there are multiple workflows, uncomment next line to ignore bower_components
+# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
+#bower_components/
+
+# RIA/Silverlight projects
+Generated_Code/
+
+# Backup & report files from converting an old project file
+# to a newer Visual Studio version. Backup files are not needed,
+# because we have git ;-)
+_UpgradeReport_Files/
+Backup*/
+UpgradeLog*.XML
+UpgradeLog*.htm
+ServiceFabricBackup/
+*.rptproj.bak
+
+# SQL Server files
+*.mdf
+*.ldf
+*.ndf
+
+# Business Intelligence projects
+*.rdl.data
+*.bim.layout
+*.bim_*.settings
+*.rptproj.rsuser
+
+# Microsoft Fakes
+FakesAssemblies/
+
+# GhostDoc plugin setting file
+*.GhostDoc.xml
+
+# Node.js Tools for Visual Studio
+.ntvs_analysis.dat
+node_modules/
+
+# Visual Studio 6 build log
+*.plg
+
+# Visual Studio 6 workspace options file
+*.opt
+
+# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
+*.vbw
+
+# Visual Studio LightSwitch build output
+**/*.HTMLClient/GeneratedArtifacts
+**/*.DesktopClient/GeneratedArtifacts
+**/*.DesktopClient/ModelManifest.xml
+**/*.Server/GeneratedArtifacts
+**/*.Server/ModelManifest.xml
+_Pvt_Extensions
+
+# Paket dependency manager
+.paket/paket.exe
+paket-files/
+
+# FAKE - F# Make
+.fake/
+
+# JetBrains Rider
+.idea/
+*.sln.iml
+
+# CodeRush personal settings
+.cr/personal
+
+# Python Tools for Visual Studio (PTVS)
+__pycache__/
+*.pyc
+
+# Cake - Uncomment if you are using it
+# tools/**
+# !tools/packages.config
+
+# Tabs Studio
+*.tss
+
+# Telerik's JustMock configuration file
+*.jmconfig
+
+# BizTalk build output
+*.btp.cs
+*.btm.cs
+*.odx.cs
+*.xsd.cs
+
+# OpenCover UI analysis results
+OpenCover/
+
+# Azure Stream Analytics local run output
+ASALocalRun/
+
+# MSBuild Binary and Structured Log
+*.binlog
+
+# NVidia Nsight GPU debugger configuration file
+*.nvuser
+
+# MFractors (Xamarin productivity tool) working folder
+.mfractor/
+
+# Local History for Visual Studio
+.localhistory/
+
+### VisualStudio ###
+## Ignore Visual Studio temporary files, build results, and
+## files generated by popular Visual Studio add-ons.
+##
+## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
+
+# User-specific files
+
+# User-specific files (MonoDevelop/Xamarin Studio)
+
+# Build results
+
+# Visual Studio 2015/2017 cache/options directory
+# Uncomment if you have tasks that create the project's static files in wwwroot
+#wwwroot/
+
+# Visual Studio 2017 auto generated files
+
+# MSTest test Results
+
+# NUNIT
+
+# Build Results of an ATL Project
+
+# Benchmark Results
+
+# .NET Core
+
+# StyleCop
+
+# Files built by Visual Studio
+
+# Chutzpah Test files
+
+# Visual C++ cache files
+
+# Visual Studio profiler
+
+# Visual Studio Trace Files
+
+# TFS 2012 Local Workspace
+
+# Guidance Automation Toolkit
+
+# ReSharper is a .NET coding add-in
+
+# JustCode is a .NET coding add-in
+
+# TeamCity is a build add-in
+
+# DotCover is a Code Coverage Tool
+
+# AxoCover is a Code Coverage Tool
+
+# Visual Studio code coverage results
+
+# NCrunch
+
+# MightyMoose
+
+# Web workbench (sass)
+
+# Installshield output folder
+
+# DocProject is a documentation generator add-in
+
+# Click-Once directory
+
+# Publish Web Output
+# Note: Comment the next line if you want to checkin your web deploy settings,
+# but database connection strings (with potential passwords) will be unencrypted
+
+# Microsoft Azure Web App publish settings. Comment the next line if you want to
+# checkin your Azure Web App publish settings, but sensitive information contained
+# in these scripts will be unencrypted
+
+# NuGet Packages
+# The packages folder can be ignored because of Package Restore
+# except build/, which is used as an MSBuild target.
+# Uncomment if necessary however generally it will be regenerated when needed
+#!**/[Pp]ackages/repositories.config
+# NuGet v3's project.json files produces more ignorable files
+
+# Microsoft Azure Build Output
+
+# Microsoft Azure Emulator
+
+# Windows Store app package directories and files
+
+# Visual Studio cache files
+# files ending in .cache can be ignored
+# but keep track of directories ending in .cache
+
+# Others
+
+# Including strong name files can present a security risk
+# (https://github.com/github/gitignore/pull/2483#issue-259490424)
+#*.snk
+
+# Since there are multiple workflows, uncomment next line to ignore bower_components
+# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
+#bower_components/
+
+# RIA/Silverlight projects
+
+# Backup & report files from converting an old project file
+# to a newer Visual Studio version. Backup files are not needed,
+# because we have git ;-)
+
+# SQL Server files
+
+# Business Intelligence projects
+
+# Microsoft Fakes
+
+# GhostDoc plugin setting file
+
+# Node.js Tools for Visual Studio
+
+# Visual Studio 6 build log
+
+# Visual Studio 6 workspace options file
+
+# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
+
+# Visual Studio LightSwitch build output
+
+# Paket dependency manager
+
+# FAKE - F# Make
+
+# JetBrains Rider
+
+# CodeRush personal settings
+
+# Python Tools for Visual Studio (PTVS)
+
+# Cake - Uncomment if you are using it
+# tools/**
+# !tools/packages.config
+
+# Tabs Studio
+
+# Telerik's JustMock configuration file
+
+# BizTalk build output
+
+# OpenCover UI analysis results
+
+# Azure Stream Analytics local run output
+
+# MSBuild Binary and Structured Log
+
+# NVidia Nsight GPU debugger configuration file
+
+# MFractors (Xamarin productivity tool) working folder
+
+# Local History for Visual Studio
+
+# End of https://www.gitignore.io/api/csharp,visualstudio
+
diff --git a/Brutario.licenseheader b/Brutario.licenseheader
new file mode 100644
index 0000000..dd78844
--- /dev/null
+++ b/Brutario.licenseheader
@@ -0,0 +1,36 @@
+extensions: designer.cs generated.cs
+extensions: .cs .cpp .h
+//
+// Copyright (c) %CurrentYear% spel werdz rite. All rights reserved. Licensed
+// under GNU Affero General Public License. See LICENSE in project
+// root for full license information, or visit
+// https://www.gnu.org/licenses/#AGPL
+//
+
+extensions: .aspx .ascx
+<%--
+
+ Copyright (c) %CurrentYear% spel werdz rite. All rights reserved. Licensed
+ under GNU Affero General Public License. See LICENSE in project
+ root for full license information, or visit
+ https://www.gnu.org/licenses/#AGPL
+
+
+--%>
+extensions: .vb
+'
+' Copyright (c) %CurrentYear% spel werdz rite. All rights reserved. Licensed
+' under GNU Affero General Public License. See LICENSE in project
+' root for full license information, or visit
+' https://www.gnu.org/licenses/#AGPL
+'
+
+extensions: .xml .config .xsd
+
diff --git a/Brutario.sln b/Brutario.sln
new file mode 100644
index 0000000..3b810e4
--- /dev/null
+++ b/Brutario.sln
@@ -0,0 +1,60 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.1.32319.34
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Snes", "src\Snes\Snes.csproj", "{ED02FB09-FBEB-423D-99AE-892BDEBEC3AF}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{6C8DF6FE-2326-4109-9C96-CD3EC8C5CFDF}"
+ ProjectSection(SolutionItems) = preProject
+ Brutario.licenseheader = Brutario.licenseheader
+ EndProjectSection
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Smas", "src\Smas\Smas.csproj", "{7966EA0F-A6FF-4DDF-A419-316A53E6E5B2}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Brutario.Core", "src\Brutario.Core\Brutario.Core.csproj", "{38080649-340D-4AC4-A86C-5B51418AC48B}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Brutario.Win", "src\Brutario.Win\Brutario.Win.csproj", "{6D2EA12A-5AF2-49B4-B522-8E46572AC6E5}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Brutario.Core.Tests", "test\Brutario.Core.Tests\Brutario.Core.Tests.csproj", "{A2521D70-CE45-4E85-86E8-6A5FD47107F7}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Snes.Tests", "test\Snes.Tests\Snes.Tests.csproj", "{8201D8A0-EC40-4429-A7F5-8D5284FC1B54}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {ED02FB09-FBEB-423D-99AE-892BDEBEC3AF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {ED02FB09-FBEB-423D-99AE-892BDEBEC3AF}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {ED02FB09-FBEB-423D-99AE-892BDEBEC3AF}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {ED02FB09-FBEB-423D-99AE-892BDEBEC3AF}.Release|Any CPU.Build.0 = Release|Any CPU
+ {7966EA0F-A6FF-4DDF-A419-316A53E6E5B2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {7966EA0F-A6FF-4DDF-A419-316A53E6E5B2}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {7966EA0F-A6FF-4DDF-A419-316A53E6E5B2}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {7966EA0F-A6FF-4DDF-A419-316A53E6E5B2}.Release|Any CPU.Build.0 = Release|Any CPU
+ {38080649-340D-4AC4-A86C-5B51418AC48B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {38080649-340D-4AC4-A86C-5B51418AC48B}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {38080649-340D-4AC4-A86C-5B51418AC48B}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {38080649-340D-4AC4-A86C-5B51418AC48B}.Release|Any CPU.Build.0 = Release|Any CPU
+ {6D2EA12A-5AF2-49B4-B522-8E46572AC6E5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {6D2EA12A-5AF2-49B4-B522-8E46572AC6E5}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {6D2EA12A-5AF2-49B4-B522-8E46572AC6E5}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {6D2EA12A-5AF2-49B4-B522-8E46572AC6E5}.Release|Any CPU.Build.0 = Release|Any CPU
+ {A2521D70-CE45-4E85-86E8-6A5FD47107F7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {A2521D70-CE45-4E85-86E8-6A5FD47107F7}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {A2521D70-CE45-4E85-86E8-6A5FD47107F7}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {A2521D70-CE45-4E85-86E8-6A5FD47107F7}.Release|Any CPU.Build.0 = Release|Any CPU
+ {8201D8A0-EC40-4429-A7F5-8D5284FC1B54}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {8201D8A0-EC40-4429-A7F5-8D5284FC1B54}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {8201D8A0-EC40-4429-A7F5-8D5284FC1B54}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {8201D8A0-EC40-4429-A7F5-8D5284FC1B54}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {3F617EB4-4FD2-4A79-8723-BA236E3EA1A9}
+ EndGlobalSection
+EndGlobal
diff --git a/Credits.rtf b/Credits.rtf
new file mode 100644
index 0000000..3287aa2
--- /dev/null
+++ b/Credits.rtf
@@ -0,0 +1,625 @@
+{\rtf1\adeflang1025\ansi\ansicpg1252\uc1\adeff0\deff0\stshfdbch31505\stshfloch31506\stshfhich31506\stshfbi0\deflang1033\deflangfe1033\themelang1033\themelangfe0\themelangcs0{\fonttbl{\f0\fbidi \froman\fcharset0\fprq2{\*\panose 02020603050405020304}Times New Roman;}{\f34\fbidi \froman\fcharset0\fprq2{\*\panose 02040503050406030204}Cambria Math;}
+{\f37\fbidi \fswiss\fcharset0\fprq2{\*\panose 020f0502020204030204}Calibri;}{\f42\fbidi \froman\fcharset0\fprq2{\*\panose 00000000000000000000}Cambria;}{\f44\fbidi \fmodern\fcharset0\fprq1{\*\panose 020b0609020204030204}Consolas;}
+{\f45\fbidi \fmodern\fcharset0\fprq1{\*\panose 00000000000000000000}Menlo{\*\falt Arial};}{\flomajor\f31500\fbidi \froman\fcharset0\fprq2{\*\panose 02020603050405020304}Times New Roman;}
+{\fdbmajor\f31501\fbidi \froman\fcharset0\fprq2{\*\panose 02020603050405020304}Times New Roman;}{\fhimajor\f31502\fbidi \froman\fcharset0\fprq2{\*\panose 00000000000000000000}Cambria;}
+{\fbimajor\f31503\fbidi \froman\fcharset0\fprq2{\*\panose 02020603050405020304}Times New Roman;}{\flominor\f31504\fbidi \froman\fcharset0\fprq2{\*\panose 02020603050405020304}Times New Roman;}
+{\fdbminor\f31505\fbidi \froman\fcharset0\fprq2{\*\panose 02020603050405020304}Times New Roman;}{\fhiminor\f31506\fbidi \fswiss\fcharset0\fprq2{\*\panose 020f0502020204030204}Calibri;}
+{\fbiminor\f31507\fbidi \froman\fcharset0\fprq2{\*\panose 02020603050405020304}Times New Roman;}{\f46\fbidi \froman\fcharset238\fprq2 Times New Roman CE;}{\f47\fbidi \froman\fcharset204\fprq2 Times New Roman Cyr;}
+{\f49\fbidi \froman\fcharset161\fprq2 Times New Roman Greek;}{\f50\fbidi \froman\fcharset162\fprq2 Times New Roman Tur;}{\f51\fbidi \froman\fcharset177\fprq2 Times New Roman (Hebrew);}{\f52\fbidi \froman\fcharset178\fprq2 Times New Roman (Arabic);}
+{\f53\fbidi \froman\fcharset186\fprq2 Times New Roman Baltic;}{\f54\fbidi \froman\fcharset163\fprq2 Times New Roman (Vietnamese);}{\f386\fbidi \froman\fcharset238\fprq2 Cambria Math CE;}{\f387\fbidi \froman\fcharset204\fprq2 Cambria Math Cyr;}
+{\f389\fbidi \froman\fcharset161\fprq2 Cambria Math Greek;}{\f390\fbidi \froman\fcharset162\fprq2 Cambria Math Tur;}{\f393\fbidi \froman\fcharset186\fprq2 Cambria Math Baltic;}{\f394\fbidi \froman\fcharset163\fprq2 Cambria Math (Vietnamese);}
+{\f416\fbidi \fswiss\fcharset238\fprq2 Calibri CE;}{\f417\fbidi \fswiss\fcharset204\fprq2 Calibri Cyr;}{\f419\fbidi \fswiss\fcharset161\fprq2 Calibri Greek;}{\f420\fbidi \fswiss\fcharset162\fprq2 Calibri Tur;}
+{\f421\fbidi \fswiss\fcharset177\fprq2 Calibri (Hebrew);}{\f422\fbidi \fswiss\fcharset178\fprq2 Calibri (Arabic);}{\f423\fbidi \fswiss\fcharset186\fprq2 Calibri Baltic;}{\f424\fbidi \fswiss\fcharset163\fprq2 Calibri (Vietnamese);}
+{\f466\fbidi \froman\fcharset238\fprq2 Cambria CE;}{\f467\fbidi \froman\fcharset204\fprq2 Cambria Cyr;}{\f469\fbidi \froman\fcharset161\fprq2 Cambria Greek;}{\f470\fbidi \froman\fcharset162\fprq2 Cambria Tur;}
+{\f473\fbidi \froman\fcharset186\fprq2 Cambria Baltic;}{\f474\fbidi \froman\fcharset163\fprq2 Cambria (Vietnamese);}{\f486\fbidi \fmodern\fcharset238\fprq1 Consolas CE;}{\f487\fbidi \fmodern\fcharset204\fprq1 Consolas Cyr;}
+{\f489\fbidi \fmodern\fcharset161\fprq1 Consolas Greek;}{\f490\fbidi \fmodern\fcharset162\fprq1 Consolas Tur;}{\f493\fbidi \fmodern\fcharset186\fprq1 Consolas Baltic;}{\f494\fbidi \fmodern\fcharset163\fprq1 Consolas (Vietnamese);}
+{\flomajor\f31508\fbidi \froman\fcharset238\fprq2 Times New Roman CE;}{\flomajor\f31509\fbidi \froman\fcharset204\fprq2 Times New Roman Cyr;}{\flomajor\f31511\fbidi \froman\fcharset161\fprq2 Times New Roman Greek;}
+{\flomajor\f31512\fbidi \froman\fcharset162\fprq2 Times New Roman Tur;}{\flomajor\f31513\fbidi \froman\fcharset177\fprq2 Times New Roman (Hebrew);}{\flomajor\f31514\fbidi \froman\fcharset178\fprq2 Times New Roman (Arabic);}
+{\flomajor\f31515\fbidi \froman\fcharset186\fprq2 Times New Roman Baltic;}{\flomajor\f31516\fbidi \froman\fcharset163\fprq2 Times New Roman (Vietnamese);}{\fdbmajor\f31518\fbidi \froman\fcharset238\fprq2 Times New Roman CE;}
+{\fdbmajor\f31519\fbidi \froman\fcharset204\fprq2 Times New Roman Cyr;}{\fdbmajor\f31521\fbidi \froman\fcharset161\fprq2 Times New Roman Greek;}{\fdbmajor\f31522\fbidi \froman\fcharset162\fprq2 Times New Roman Tur;}
+{\fdbmajor\f31523\fbidi \froman\fcharset177\fprq2 Times New Roman (Hebrew);}{\fdbmajor\f31524\fbidi \froman\fcharset178\fprq2 Times New Roman (Arabic);}{\fdbmajor\f31525\fbidi \froman\fcharset186\fprq2 Times New Roman Baltic;}
+{\fdbmajor\f31526\fbidi \froman\fcharset163\fprq2 Times New Roman (Vietnamese);}{\fhimajor\f31528\fbidi \froman\fcharset238\fprq2 Cambria CE;}{\fhimajor\f31529\fbidi \froman\fcharset204\fprq2 Cambria Cyr;}
+{\fhimajor\f31531\fbidi \froman\fcharset161\fprq2 Cambria Greek;}{\fhimajor\f31532\fbidi \froman\fcharset162\fprq2 Cambria Tur;}{\fhimajor\f31535\fbidi \froman\fcharset186\fprq2 Cambria Baltic;}
+{\fhimajor\f31536\fbidi \froman\fcharset163\fprq2 Cambria (Vietnamese);}{\fbimajor\f31538\fbidi \froman\fcharset238\fprq2 Times New Roman CE;}{\fbimajor\f31539\fbidi \froman\fcharset204\fprq2 Times New Roman Cyr;}
+{\fbimajor\f31541\fbidi \froman\fcharset161\fprq2 Times New Roman Greek;}{\fbimajor\f31542\fbidi \froman\fcharset162\fprq2 Times New Roman Tur;}{\fbimajor\f31543\fbidi \froman\fcharset177\fprq2 Times New Roman (Hebrew);}
+{\fbimajor\f31544\fbidi \froman\fcharset178\fprq2 Times New Roman (Arabic);}{\fbimajor\f31545\fbidi \froman\fcharset186\fprq2 Times New Roman Baltic;}{\fbimajor\f31546\fbidi \froman\fcharset163\fprq2 Times New Roman (Vietnamese);}
+{\flominor\f31548\fbidi \froman\fcharset238\fprq2 Times New Roman CE;}{\flominor\f31549\fbidi \froman\fcharset204\fprq2 Times New Roman Cyr;}{\flominor\f31551\fbidi \froman\fcharset161\fprq2 Times New Roman Greek;}
+{\flominor\f31552\fbidi \froman\fcharset162\fprq2 Times New Roman Tur;}{\flominor\f31553\fbidi \froman\fcharset177\fprq2 Times New Roman (Hebrew);}{\flominor\f31554\fbidi \froman\fcharset178\fprq2 Times New Roman (Arabic);}
+{\flominor\f31555\fbidi \froman\fcharset186\fprq2 Times New Roman Baltic;}{\flominor\f31556\fbidi \froman\fcharset163\fprq2 Times New Roman (Vietnamese);}{\fdbminor\f31558\fbidi \froman\fcharset238\fprq2 Times New Roman CE;}
+{\fdbminor\f31559\fbidi \froman\fcharset204\fprq2 Times New Roman Cyr;}{\fdbminor\f31561\fbidi \froman\fcharset161\fprq2 Times New Roman Greek;}{\fdbminor\f31562\fbidi \froman\fcharset162\fprq2 Times New Roman Tur;}
+{\fdbminor\f31563\fbidi \froman\fcharset177\fprq2 Times New Roman (Hebrew);}{\fdbminor\f31564\fbidi \froman\fcharset178\fprq2 Times New Roman (Arabic);}{\fdbminor\f31565\fbidi \froman\fcharset186\fprq2 Times New Roman Baltic;}
+{\fdbminor\f31566\fbidi \froman\fcharset163\fprq2 Times New Roman (Vietnamese);}{\fhiminor\f31568\fbidi \fswiss\fcharset238\fprq2 Calibri CE;}{\fhiminor\f31569\fbidi \fswiss\fcharset204\fprq2 Calibri Cyr;}
+{\fhiminor\f31571\fbidi \fswiss\fcharset161\fprq2 Calibri Greek;}{\fhiminor\f31572\fbidi \fswiss\fcharset162\fprq2 Calibri Tur;}{\fhiminor\f31573\fbidi \fswiss\fcharset177\fprq2 Calibri (Hebrew);}
+{\fhiminor\f31574\fbidi \fswiss\fcharset178\fprq2 Calibri (Arabic);}{\fhiminor\f31575\fbidi \fswiss\fcharset186\fprq2 Calibri Baltic;}{\fhiminor\f31576\fbidi \fswiss\fcharset163\fprq2 Calibri (Vietnamese);}
+{\fbiminor\f31578\fbidi \froman\fcharset238\fprq2 Times New Roman CE;}{\fbiminor\f31579\fbidi \froman\fcharset204\fprq2 Times New Roman Cyr;}{\fbiminor\f31581\fbidi \froman\fcharset161\fprq2 Times New Roman Greek;}
+{\fbiminor\f31582\fbidi \froman\fcharset162\fprq2 Times New Roman Tur;}{\fbiminor\f31583\fbidi \froman\fcharset177\fprq2 Times New Roman (Hebrew);}{\fbiminor\f31584\fbidi \froman\fcharset178\fprq2 Times New Roman (Arabic);}
+{\fbiminor\f31585\fbidi \froman\fcharset186\fprq2 Times New Roman Baltic;}{\fbiminor\f31586\fbidi \froman\fcharset163\fprq2 Times New Roman (Vietnamese);}}{\colortbl;\red0\green0\blue0;\red0\green0\blue255;\red0\green255\blue255;\red0\green255\blue0;
+\red255\green0\blue255;\red255\green0\blue0;\red255\green255\blue0;\red255\green255\blue255;\red0\green0\blue128;\red0\green128\blue128;\red0\green128\blue0;\red128\green0\blue128;\red128\green0\blue0;\red128\green128\blue0;\red128\green128\blue128;
+\red192\green192\blue192;\red0\green0\blue0;\red0\green0\blue0;\red221\green217\blue195;\ctextone\ctint166\cshade255\red89\green89\blue89;\ctextone\ctint165\cshade255\red90\green90\blue90;\chyperlink\ctint255\cshade255\red0\green0\blue255;
+\red96\green94\blue92;\red225\green223\blue221;}{\*\defchp \fs22\loch\af31506\hich\af31506\dbch\af31505 }{\*\defpap \ql \li0\ri0\widctlpar\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0 }\noqfpromote {\stylesheet{
+\ql \li0\ri0\widctlpar\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0 \rtlch\fcs1 \af0\afs24\alang1025 \ltrch\fcs0 \fs24\lang1033\langfe1033\loch\f31506\hich\af31506\dbch\af31505\cgrid\langnp1033\langfenp1033
+\snext0 \sqformat \spriority0 \styrsid6830075 Normal;}{\s1\ql \li0\ri0\sb240\sa60\keepn\widctlpar\wrapdefault\aspalpha\aspnum\faauto\outlinelevel0\adjustright\rin0\lin0\itap0 \rtlch\fcs1 \ab\af0\afs32\alang1025 \ltrch\fcs0
+\b\fs32\lang1033\langfe1033\kerning32\loch\f31502\hich\af31502\dbch\af31501\cgrid\langnp1033\langfenp1033 \sbasedon0 \snext0 \slink15 \sqformat \spriority9 \styrsid6830075 heading 1;}{
+\s2\ql \li0\ri0\sb240\sa60\keepn\widctlpar\wrapdefault\aspalpha\aspnum\faauto\outlinelevel1\adjustright\rin0\lin0\itap0 \rtlch\fcs1 \ab\ai\af0\afs28\alang1025 \ltrch\fcs0
+\b\i\fs28\lang1033\langfe1033\loch\f31502\hich\af31502\dbch\af31501\cgrid\langnp1033\langfenp1033 \sbasedon0 \snext0 \slink16 \sunhideused \sqformat \spriority9 \styrsid6830075 heading 2;}{
+\s3\ql \li0\ri0\sb240\sa60\keepn\widctlpar\wrapdefault\aspalpha\aspnum\faauto\outlinelevel2\adjustright\rin0\lin0\itap0 \rtlch\fcs1 \ab\af0\afs26\alang1025 \ltrch\fcs0
+\b\fs26\lang1033\langfe1033\loch\f31502\hich\af31502\dbch\af31501\cgrid\langnp1033\langfenp1033 \sbasedon0 \snext0 \slink17 \ssemihidden \sunhideused \sqformat \spriority9 \styrsid6830075 heading 3;}{
+\s4\ql \li0\ri0\sb240\sa60\keepn\widctlpar\wrapdefault\aspalpha\aspnum\faauto\outlinelevel3\adjustright\rin0\lin0\itap0 \rtlch\fcs1 \ab\af0\afs28\alang1025 \ltrch\fcs0
+\b\fs28\lang1033\langfe1033\loch\f31506\hich\af31506\dbch\af31505\cgrid\langnp1033\langfenp1033 \sbasedon0 \snext0 \slink18 \ssemihidden \sunhideused \sqformat \spriority9 \styrsid6830075 heading 4;}{
+\s5\ql \li0\ri0\sb240\sa60\widctlpar\wrapdefault\aspalpha\aspnum\faauto\outlinelevel4\adjustright\rin0\lin0\itap0 \rtlch\fcs1 \ab\ai\af0\afs26\alang1025 \ltrch\fcs0
+\b\i\fs26\lang1033\langfe1033\loch\f31506\hich\af31506\dbch\af31505\cgrid\langnp1033\langfenp1033 \sbasedon0 \snext0 \slink19 \ssemihidden \sunhideused \sqformat \spriority9 \styrsid6830075 heading 5;}{
+\s6\ql \li0\ri0\sb240\sa60\widctlpar\wrapdefault\aspalpha\aspnum\faauto\outlinelevel5\adjustright\rin0\lin0\itap0 \rtlch\fcs1 \ab\af0\afs22\alang1025 \ltrch\fcs0
+\b\fs22\lang1033\langfe1033\loch\f31506\hich\af31506\dbch\af31505\cgrid\langnp1033\langfenp1033 \sbasedon0 \snext0 \slink20 \ssemihidden \sunhideused \sqformat \spriority9 \styrsid6830075 heading 6;}{
+\s7\ql \li0\ri0\sb240\sa60\widctlpar\wrapdefault\aspalpha\aspnum\faauto\outlinelevel6\adjustright\rin0\lin0\itap0 \rtlch\fcs1 \af0\afs24\alang1025 \ltrch\fcs0 \fs24\lang1033\langfe1033\loch\f31506\hich\af31506\dbch\af31505\cgrid\langnp1033\langfenp1033
+\sbasedon0 \snext0 \slink21 \ssemihidden \sunhideused \sqformat \spriority9 \styrsid6830075 heading 7;}{\s8\ql \li0\ri0\sb240\sa60\widctlpar\wrapdefault\aspalpha\aspnum\faauto\outlinelevel7\adjustright\rin0\lin0\itap0 \rtlch\fcs1 \ai\af0\afs24\alang1025
+\ltrch\fcs0 \i\fs24\lang1033\langfe1033\loch\f31506\hich\af31506\dbch\af31505\cgrid\langnp1033\langfenp1033 \sbasedon0 \snext0 \slink22 \ssemihidden \sunhideused \sqformat \spriority9 \styrsid6830075 heading 8;}{
+\s9\ql \li0\ri0\sb240\sa60\widctlpar\wrapdefault\aspalpha\aspnum\faauto\outlinelevel8\adjustright\rin0\lin0\itap0 \rtlch\fcs1 \af0\afs22\alang1025 \ltrch\fcs0 \fs22\lang1033\langfe1033\loch\f31502\hich\af31502\dbch\af31501\cgrid\langnp1033\langfenp1033
+\sbasedon0 \snext0 \slink23 \ssemihidden \sunhideused \sqformat \spriority9 \styrsid6830075 heading 9;}{\*\cs10 \additive \sunhideused \spriority1 Default Paragraph Font;}{\*
+\ts11\tsrowd\trftsWidthB3\trpaddl108\trpaddr108\trpaddfl3\trpaddft3\trpaddfb3\trpaddfr3\trcbpat1\trcfpat1\tblind0\tblindtype3\tsvertalt\tsbrdrt\tsbrdrl\tsbrdrb\tsbrdrr\tsbrdrdgl\tsbrdrdgr\tsbrdrh\tsbrdrv
+\ql \li0\ri0\widctlpar\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0 \rtlch\fcs1 \af0\afs22\alang1025 \ltrch\fcs0 \fs22\lang1033\langfe1033\loch\f31506\hich\af31506\dbch\af31505\cgrid\langnp1033\langfenp1033
+\snext11 \ssemihidden \sunhideused Normal Table;}{\*\cs15 \additive \rtlch\fcs1 \ab\af0\afs32 \ltrch\fcs0 \b\fs32\kerning32\loch\f31502\hich\af31502\dbch\af31501 \sbasedon10 \slink1 \slocked \spriority9 \styrsid6830075 Heading 1 Char;}{\*\cs16 \additive
+\rtlch\fcs1 \ab\ai\af0\afs28 \ltrch\fcs0 \b\i\fs28\loch\f31502\hich\af31502\dbch\af31501 \sbasedon10 \slink2 \slocked \spriority9 \styrsid6830075 Heading 2 Char;}{\*\cs17 \additive \rtlch\fcs1 \ab\af0\afs26 \ltrch\fcs0
+\b\fs26\loch\f31502\hich\af31502\dbch\af31501 \sbasedon10 \slink3 \slocked \ssemihidden \spriority9 \styrsid6830075 Heading 3 Char;}{\*\cs18 \additive \rtlch\fcs1 \ab\af0\afs28 \ltrch\fcs0 \b\fs28
+\sbasedon10 \slink4 \slocked \ssemihidden \spriority9 \styrsid6830075 Heading 4 Char;}{\*\cs19 \additive \rtlch\fcs1 \ab\ai\af0\afs26 \ltrch\fcs0 \b\i\fs26 \sbasedon10 \slink5 \slocked \ssemihidden \spriority9 \styrsid6830075 Heading 5 Char;}{\*\cs20
+\additive \rtlch\fcs1 \ab\af0 \ltrch\fcs0 \b \sbasedon10 \slink6 \slocked \ssemihidden \spriority9 \styrsid6830075 Heading 6 Char;}{\*\cs21 \additive \rtlch\fcs1 \af0\afs24 \ltrch\fcs0 \fs24
+\sbasedon10 \slink7 \slocked \ssemihidden \spriority9 \styrsid6830075 Heading 7 Char;}{\*\cs22 \additive \rtlch\fcs1 \ai\af0\afs24 \ltrch\fcs0 \i\fs24 \sbasedon10 \slink8 \slocked \ssemihidden \spriority9 \styrsid6830075 Heading 8 Char;}{\*\cs23
+\additive \rtlch\fcs1 \af0 \ltrch\fcs0 \loch\f31502\hich\af31502\dbch\af31501 \sbasedon10 \slink9 \slocked \ssemihidden \spriority9 \styrsid6830075 Heading 9 Char;}{
+\s24\ql \li0\ri0\sa20\keep\widctlpar\wrapdefault\hyphpar0\aspalpha\faroman\adjustright\rin0\lin0\itap0 \cbpat19 \rtlch\fcs1 \af45\afs18\alang1025 \ltrch\fcs0 \fs18\lang1024\langfe1024\loch\f44\hich\af44\dbch\af31505\cgrid\noproof\langnp1033\langfenp1033
+\sbasedon0 \snext24 \sautoupd \spriority0 \styrsid217866 code;}{\s25\ql \li0\ri0\widctlpar\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0 \rtlch\fcs1 \ab\af0\afs24\alang1025 \ltrch\fcs0
+\b\scaps\fs24\expnd1\expndtw6\cf20\lang1033\langfe1033\loch\f31506\hich\af31506\dbch\af31505\cgrid\langnp1033\langfenp1033 \sbasedon0 \snext0 \ssemihidden \sunhideused \spriority35 \styrsid6830075 caption;}{
+\s26\qc \li0\ri0\sb240\sa60\widctlpar\wrapdefault\aspalpha\aspnum\faauto\outlinelevel0\adjustright\rin0\lin0\itap0 \rtlch\fcs1 \ab\af0\afs32\alang1025 \ltrch\fcs0
+\b\fs32\lang1033\langfe1033\kerning28\loch\f31502\hich\af31502\dbch\af31501\cgrid\langnp1033\langfenp1033 \sbasedon0 \snext0 \slink27 \sqformat \spriority10 \styrsid6830075 Title;}{\*\cs27 \additive \rtlch\fcs1 \ab\af0\afs32 \ltrch\fcs0
+\b\fs32\kerning28\loch\f31502\hich\af31502\dbch\af31501 \sbasedon10 \slink26 \slocked \spriority10 \styrsid6830075 Title Char;}{\s28\qc \li0\ri0\sa60\widctlpar\wrapdefault\aspalpha\aspnum\faauto\outlinelevel1\adjustright\rin0\lin0\itap0 \rtlch\fcs1
+\af0\afs24\alang1025 \ltrch\fcs0 \fs24\lang1033\langfe1033\loch\f31502\hich\af31502\dbch\af31501\cgrid\langnp1033\langfenp1033 \sbasedon0 \snext0 \slink29 \sqformat \spriority11 \styrsid6830075 Subtitle;}{\*\cs29 \additive \rtlch\fcs1 \af0\afs24
+\ltrch\fcs0 \fs24\loch\f31502\hich\af31502\dbch\af31501 \sbasedon10 \slink28 \slocked \spriority11 \styrsid6830075 Subtitle Char;}{\*\cs30 \additive \rtlch\fcs1 \ab\af0 \ltrch\fcs0 \b \sbasedon10 \sqformat \spriority22 \styrsid6830075 Strong;}{\*\cs31
+\additive \rtlch\fcs1 \ai\af0 \ltrch\fcs0 \b\i\f31506 \sbasedon10 \sqformat \spriority20 \styrsid6830075 Emphasis;}{\s32\ql \li0\ri0\widctlpar\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0 \rtlch\fcs1 \af0\afs32\alang1025 \ltrch\fcs0
+\fs24\lang1033\langfe1033\loch\f31506\hich\af31506\dbch\af31505\cgrid\langnp1033\langfenp1033 \sbasedon0 \snext32 \slink33 \sqformat \spriority1 \styrsid6830075 No Spacing;}{\*\cs33 \additive \rtlch\fcs1 \af0\afs32 \ltrch\fcs0 \fs32
+\sbasedon10 \slink32 \slocked \spriority1 \styrsid217866 No Spacing Char;}{\s34\ql \li720\ri0\widctlpar\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin720\itap0\contextualspace \rtlch\fcs1 \af0\afs24\alang1025 \ltrch\fcs0
+\fs24\lang1033\langfe1033\loch\f31506\hich\af31506\dbch\af31505\cgrid\langnp1033\langfenp1033 \sbasedon0 \snext34 \sqformat \spriority34 \styrsid6830075 List Paragraph;}{
+\s35\ql \li0\ri0\widctlpar\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0 \rtlch\fcs1 \af0\afs24\alang1025 \ltrch\fcs0 \i\fs24\lang1033\langfe1033\loch\f31506\hich\af31506\dbch\af31505\cgrid\langnp1033\langfenp1033
+\sbasedon0 \snext0 \slink36 \sqformat \spriority29 \styrsid6830075 Quote;}{\*\cs36 \additive \rtlch\fcs1 \af0\afs24 \ltrch\fcs0 \i\fs24 \sbasedon10 \slink35 \slocked \spriority29 \styrsid6830075 Quote Char;}{
+\s37\ql \li720\ri720\widctlpar\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin720\lin720\itap0 \rtlch\fcs1 \af0\afs22\alang1025 \ltrch\fcs0 \b\i\fs24\lang1033\langfe1033\loch\f31506\hich\af31506\dbch\af31505\cgrid\langnp1033\langfenp1033
+\sbasedon0 \snext0 \slink38 \sqformat \spriority30 \styrsid6830075 Intense Quote;}{\*\cs38 \additive \rtlch\fcs1 \af0 \ltrch\fcs0 \b\i\fs24 \sbasedon10 \slink37 \slocked \spriority30 \styrsid6830075 Intense Quote Char;}{\*\cs39 \additive \rtlch\fcs1 \af0
+\ltrch\fcs0 \i\cf21 \sbasedon10 \sqformat \spriority19 \styrsid6830075 Subtle Emphasis;}{\*\cs40 \additive \rtlch\fcs1 \af0\afs24 \ltrch\fcs0 \b\i\fs24\ul \sbasedon10 \sqformat \spriority21 \styrsid6830075 Intense Emphasis;}{\*\cs41 \additive \rtlch\fcs1
+\af0\afs24 \ltrch\fcs0 \fs24\ul \sbasedon10 \sqformat \spriority31 \styrsid6830075 Subtle Reference;}{\*\cs42 \additive \rtlch\fcs1 \af0 \ltrch\fcs0 \b\fs24\ul \sbasedon10 \sqformat \spriority32 \styrsid6830075 Intense Reference;}{\*\cs43 \additive
+\rtlch\fcs1 \af0\afs24 \ltrch\fcs0 \b\i\fs24\loch\f31502\hich\af31502\dbch\af31501 \sbasedon10 \sqformat \spriority33 \styrsid6830075 Book Title;}{\s44\ql \li0\ri0\sb240\sa60\keepn\widctlpar\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0
+\rtlch\fcs1 \ab\af0\afs32\alang1025 \ltrch\fcs0 \b\fs32\lang1033\langfe1033\kerning32\loch\f31502\hich\af31502\dbch\af31501\cgrid\langnp1033\langfenp1033 \sbasedon1 \snext0 \ssemihidden \sunhideused \sqformat \spriority39 \styrsid6830075 TOC Heading;}{\*
+\cs45 \additive \rtlch\fcs1 \af0 \ltrch\fcs0 \ul\cf22 \sbasedon10 \sunhideused \styrsid6830075 Hyperlink;}{\*\cs46 \additive \rtlch\fcs1 \af0 \ltrch\fcs0 \cf23\chshdng0\chcfpat0\chcbpat24 \sbasedon10 \ssemihidden \sunhideused \styrsid6830075
+Unresolved Mention;}}{\*\rsidtbl \rsid217866\rsid855346\rsid1127478\rsid1798261\rsid2109769\rsid2123635\rsid2124194\rsid4392603\rsid4869634\rsid5193884\rsid5592063\rsid5639150\rsid5925484\rsid6295126\rsid6830075\rsid7094559\rsid7241628\rsid7748460
+\rsid8019431\rsid8278635\rsid8979329\rsid9972925\rsid10645819\rsid11485576\rsid12471421\rsid12745309\rsid13138301\rsid13263836\rsid14107051\rsid14120838\rsid14638294\rsid15275006\rsid15795588\rsid15888887\rsid16341622\rsid16348870}{\mmathPr\mmathFont34
+\mbrkBin0\mbrkBinSub0\msmallFrac0\mdispDef1\mlMargin0\mrMargin0\mdefJc1\mwrapIndent1440\mintLim0\mnaryLim1}{\info{\author Nelson Garcia}{\operator Nelson Garcia}{\creatim\yr2022\mo3\dy9\hr10\min45}{\revtim\yr2024\mo8\dy25\hr14\min58}{\version8}{\edmins93}
+{\nofpages2}{\nofwords451}{\nofchars2575}{\nofcharsws3020}{\vern101}}{\*\xmlnstbl {\xmlns1 http://schemas.microsoft.com/office/word/2003/wordml}}\paperw12240\paperh15840\margl1440\margr1440\margt1440\margb1440\gutter0\ltrsect
+\widowctrl\ftnbj\aenddoc\trackmoves0\trackformatting1\donotembedsysfont1\relyonvml0\donotembedlingdata0\grfdocevents0\validatexml1\showplaceholdtext0\ignoremixedcontent0\saveinvalidxml0\showxmlerrors1\noxlattoyen
+\expshrtn\noultrlspc\dntblnsbdb\nospaceforul\formshade\horzdoc\dgmargin\dghspace180\dgvspace180\dghorigin1440\dgvorigin1440\dghshow1\dgvshow1
+\jexpand\viewkind1\viewscale174\viewzk2\pgbrdrhead\pgbrdrfoot\splytwnine\ftnlytwnine\htmautsp\nolnhtadjtbl\useltbaln\alntblind\lytcalctblwd\lyttblrtgr\lnbrkrule\nobrkwrptbl\snaptogridincell\allowfieldendsel
+\wrppunct\asianbrkrule\rsidroot6830075\newtblstyruls\nogrowautofit\usenormstyforlist\noindnmbrts\felnbrelev\nocxsptable\indrlsweleven\noafcnsttbl\afelev\utinl\hwelev\spltpgpar\notcvasp\notbrkcnstfrctbl\notvatxbx\krnprsnet\cachedcolbal \nouicompat \fet0
+{\*\wgrffmtfilter 2450}\nofeaturethrottle1\ilfomacatclnup0\ltrpar \sectd \ltrsect\linex0\endnhere\sectlinegrid360\sectdefaultcl\sftnbj {\*\pnseclvl1\pnucrm\pnstart1\pnindent720\pnhang {\pntxta .}}{\*\pnseclvl2\pnucltr\pnstart1\pnindent720\pnhang
+{\pntxta .}}{\*\pnseclvl3\pndec\pnstart1\pnindent720\pnhang {\pntxta .}}{\*\pnseclvl4\pnlcltr\pnstart1\pnindent720\pnhang {\pntxta )}}{\*\pnseclvl5\pndec\pnstart1\pnindent720\pnhang {\pntxtb (}{\pntxta )}}{\*\pnseclvl6\pnlcltr\pnstart1\pnindent720\pnhang
+{\pntxtb (}{\pntxta )}}{\*\pnseclvl7\pnlcrm\pnstart1\pnindent720\pnhang {\pntxtb (}{\pntxta )}}{\*\pnseclvl8\pnlcltr\pnstart1\pnindent720\pnhang {\pntxtb (}{\pntxta )}}{\*\pnseclvl9\pnlcrm\pnstart1\pnindent720\pnhang {\pntxtb (}{\pntxta )}}
+\pard\plain \ltrpar\s26\qc \li0\ri0\sb240\sa60\widctlpar\wrapdefault\aspalpha\aspnum\faauto\outlinelevel0\adjustright\rin0\lin0\itap0\pararsid6830075 \rtlch\fcs1 \ab\af0\afs32\alang1025 \ltrch\fcs0
+\b\fs32\lang1033\langfe1033\kerning28\loch\af31502\hich\af31502\dbch\af31501\cgrid\langnp1033\langfenp1033 {\rtlch\fcs1 \af0 \ltrch\fcs0 \insrsid6830075 \hich\af31502\dbch\af31501\loch\f31502 Credits}{\rtlch\fcs1 \af0 \ltrch\fcs0 \insrsid12745309
+\par }\pard\plain \ltrpar\s1\ql \li0\ri0\sb240\sa60\keepn\widctlpar\wrapdefault\aspalpha\aspnum\faauto\outlinelevel0\adjustright\rin0\lin0\itap0\pararsid6830075 \rtlch\fcs1 \ab\af0\afs32\alang1025 \ltrch\fcs0
+\b\fs32\lang1033\langfe1033\kerning32\loch\af31502\hich\af31502\dbch\af31501\cgrid\langnp1033\langfenp1033 {\rtlch\fcs1 \af0 \ltrch\fcs0 \insrsid6830075 \hich\af31502\dbch\af31501\loch\f31502 Program}{\rtlch\fcs1 \af0 \ltrch\fcs0
+\insrsid6830075\charrsid6830075
+\par }\pard\plain \ltrpar\ql \li0\ri0\widctlpar\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0\pararsid6830075 \rtlch\fcs1 \af0\afs24\alang1025 \ltrch\fcs0
+\fs24\lang1033\langfe1033\loch\af31506\hich\af31506\dbch\af31505\cgrid\langnp1033\langfenp1033 {\rtlch\fcs1 \af0 \ltrch\fcs0 \insrsid6830075
+\par }{\rtlch\fcs1 \ab\af0 \ltrch\fcs0 \cs30\b\insrsid6830075\charrsid6830075 \hich\af31506\dbch\af31505\loch\f31506 Title}{\rtlch\fcs1 \af0 \ltrch\fcs0 \insrsid6830075 \hich\af31506\dbch\af31505\loch\f31506
+: Brutario (A brute-force Super Mario All-Stars Editor)
+\par }{\rtlch\fcs1 \ab\af0 \ltrch\fcs0 \cs30\b\insrsid6830075\charrsid6830075 \hich\af31506\dbch\af31505\loch\f31506 Url}{\rtlch\fcs1 \af0 \ltrch\fcs0 \insrsid6830075 \hich\af31506\dbch\af31505\loch\f31506 : }{\field{\*\fldinst {\rtlch\fcs1 \af0 \ltrch\fcs0
+\insrsid6830075 \hich\af31506\dbch\af31505\loch\f31506 HYPERLINK "}{\rtlch\fcs1 \af0 \ltrch\fcs0 \insrsid6830075\charrsid6830075 \hich\af31506\dbch\af31505\loch\f31506 https://github.com/Maseya/Brutario}{\rtlch\fcs1 \af0 \ltrch\fcs0 \insrsid6830075
+\hich\af31506\dbch\af31505\loch\f31506 " }{\rtlch\fcs1 \af0 \ltrch\fcs0 \insrsid13263836 {\*\datafield
+00d0c9ea79f9bace118c8200aa004ba90b0200000003000000e0c9ea79f9bace118c8200aa004ba90b5e000000680074007400700073003a002f002f006700690074006800750062002e0063006f006d002f004d00610073006500790061002f0042007200750074006100720069006f000000795881f43b1d7f48af2c825d
+c485276300000000a5ab000300000000}}}{\fldrslt {\rtlch\fcs1 \af0 \ltrch\fcs0 \cs45\ul\cf22\insrsid6830075\charrsid1798261 \hich\af31506\dbch\af31505\loch\f31506 https://github.com/Maseya/Brutario}}}\sectd \ltrsect
+\linex0\endnhere\sectlinegrid360\sectdefaultcl\sftnbj {\rtlch\fcs1 \af0 \ltrch\fcs0 \insrsid6830075
+\par }{\rtlch\fcs1 \ab\af0 \ltrch\fcs0 \cs30\b\insrsid6830075\charrsid6830075 \hich\af31506\dbch\af31505\loch\f31506 Organization}{\rtlch\fcs1 \af0 \ltrch\fcs0 \insrsid6830075 \hich\af31506\dbch\af31505\loch\f31506 : Maseya (}{\field{\*\fldinst {\rtlch\fcs1
+\af0 \ltrch\fcs0 \insrsid6830075 \hich\af31506\dbch\af31505\loch\f31506 HYPERLINK "}{\rtlch\fcs1 \af0 \ltrch\fcs0 \insrsid6830075\charrsid6830075 \hich\af31506\dbch\af31505\loch\f31506 https://github.com/Maseya}{\rtlch\fcs1 \af0 \ltrch\fcs0
+\insrsid6830075 \hich\af31506\dbch\af31505\loch\f31506 " }{\rtlch\fcs1 \af0 \ltrch\fcs0 \insrsid13263836 {\*\datafield
+00d0c9ea79f9bace118c8200aa004ba90b0200000003000000e0c9ea79f9bace118c8200aa004ba90b4c000000680074007400700073003a002f002f006700690074006800750062002e0063006f006d002f004d00610073006500790061000000795881f43b1d7f48af2c825dc485276300000000a5ab00030000005c}}
+}{\fldrslt {\rtlch\fcs1 \af0 \ltrch\fcs0 \cs45\ul\cf22\insrsid6830075\charrsid1798261 \hich\af31506\dbch\af31505\loch\f31506 https://github.com/Maseya}}}\sectd \ltrsect\linex0\endnhere\sectlinegrid360\sectdefaultcl\sftnbj {\rtlch\fcs1 \af0 \ltrch\fcs0
+\insrsid6830075 \hich\af31506\dbch\af31505\loch\f31506 )
+\par
+\par }\pard\plain \ltrpar\s1\ql \li0\ri0\sb240\sa60\keepn\widctlpar\wrapdefault\aspalpha\aspnum\faauto\outlinelevel0\adjustright\rin0\lin0\itap0\pararsid6830075 \rtlch\fcs1 \ab\af0\afs32\alang1025 \ltrch\fcs0
+\b\fs32\lang1033\langfe1033\kerning32\loch\af31502\hich\af31502\dbch\af31501\cgrid\langnp1033\langfenp1033 {\rtlch\fcs1 \af0 \ltrch\fcs0 \insrsid6830075 \hich\af31502\dbch\af31501\loch\f31502 Icons used
+\par }\pard\plain \ltrpar\s2\ql \li0\ri0\sb240\sa60\keepn\widctlpar\wrapdefault\aspalpha\aspnum\faauto\outlinelevel1\adjustright\rin0\lin0\itap0\pararsid9972925 \rtlch\fcs1 \ab\ai\af0\afs28\alang1025 \ltrch\fcs0
+\b\i\fs28\lang1033\langfe1033\loch\af31502\hich\af31502\dbch\af31501\cgrid\langnp1033\langfenp1033 {\rtlch\fcs1 \af0 \ltrch\fcs0 \insrsid14107051 \hich\af31502\dbch\af31501\loch\f31502 Font Awesome}{\rtlch\fcs1 \af0 \ltrch\fcs0 \insrsid9972925
+\par }\pard\plain \ltrpar\ql \li0\ri0\widctlpar\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0\pararsid9972925 \rtlch\fcs1 \af0\afs24\alang1025 \ltrch\fcs0
+\fs24\lang1033\langfe1033\loch\af31506\hich\af31506\dbch\af31505\cgrid\langnp1033\langfenp1033 {\rtlch\fcs1 \ab\af0 \ltrch\fcs0 \cs30\b\insrsid9972925\charrsid15795588 \hich\af31506\dbch\af31505\loch\f31506 Folder Open}{\rtlch\fcs1 \af0 \ltrch\fcs0
+\insrsid9972925 \hich\af31506\dbch\af31505\loch\f31506 : }{\field\flddirty{\*\fldinst {\rtlch\fcs1 \af0 \ltrch\fcs0 \insrsid9972925 \hich\af31506\dbch\af31505\loch\f31506 HYPERLINK "}{\rtlch\fcs1 \af0 \ltrch\fcs0 \insrsid9972925\charrsid15795588
+\hich\af31506\dbch\af31505\loch\f31506 https://fontawesome.com/icons/folder-open?s=solid}{\rtlch\fcs1 \af0 \ltrch\fcs0 \insrsid9972925 \hich\af31506\dbch\af31505\loch\f31506 " }{\rtlch\fcs1 \af0 \ltrch\fcs0 \insrsid5193884 {\*\datafield
+00d0c9ea79f9bace118c8200aa004ba90b0200000003000000e0c9ea79f9bace118c8200aa004ba90b7c000000680074007400700073003a002f002f0066006f006e00740061007700650073006f006d0065002e0063006f006d002f00690063006f006e0073002f0066006f006c006400650072002d006f00700065006e00
+3f0073003d0073006f006c00690064000000795881f43b1d7f48af2c825dc485276300000000a5ab000300}}}{\fldrslt {\rtlch\fcs1 \af0 \ltrch\fcs0 \cs45\ul\cf22\insrsid9972925\charrsid14120838 \hich\af31506\dbch\af31505\loch\f31506
+https://fontawesome.com/icons/folder-open?s=solid}}}\sectd \ltrsect\linex0\endnhere\sectlinegrid360\sectdefaultcl\sftnbj {\rtlch\fcs1 \af0 \ltrch\fcs0 \insrsid9972925 \tab \tab \tab }{\rtlch\fcs1 \af0 \ltrch\fcs0
+\lang1024\langfe1024\noproof\insrsid11485576\charrsid7695206 {\*\shppict{\pict{\*\picprop\shplid1040{\sp{\sn shapeType}{\sv 75}}{\sp{\sn fFlipH}{\sv 0}}{\sp{\sn fFlipV}{\sv 0}}{\sp{\sn fLockRotation}{\sv 0}}{\sp{\sn fLockAspectRatio}{\sv 1}}
+{\sp{\sn fLockPosition}{\sv 0}}{\sp{\sn fLockAgainstSelect}{\sv 0}}{\sp{\sn fLockCropping}{\sv 0}}{\sp{\sn fLockVerticies}{\sv 0}}{\sp{\sn fLockAgainstGrouping}{\sv 0}}{\sp{\sn pictureGray}{\sv 0}}{\sp{\sn pictureBiLevel}{\sv 0}}{\sp{\sn fFilled}{\sv 0}}
+{\sp{\sn fNoFillHitTest}{\sv 0}}{\sp{\sn fLine}{\sv 0}}{\sp{\sn wzName}{\sv Picture 13}}{\sp{\sn dhgt}{\sv 251658240}}{\sp{\sn fHidden}{\sv 0}}{\sp{\sn fLayoutInCell}{\sv 1}}}\picscalex100\picscaley100\piccropl0\piccropr0\piccropt0\piccropb0
+\picw423\pich423\picwgoal240\pichgoal240\pngblip\bliptag587145968{\*\blipuid 22ff22f09875d6aa09423248f4f0299b}89504e470d0a1a0a0000000d49484452000000100000001008060000001ff3ff610000008b49444154388d631814e03c0303c37f2c783e318e63846ac4053630
+30305c4493bb001587036cb613c20930cd4c386c26041460f22c680a173030303c24a019040e60f30228d0f613703a3a7e0ff3c20768603960d845040099d4c0
+c0c0701f8b0d8430481fc37b06068602020ab161903e01064a6d0701726c075908b61d04c8b11d9e90061830303000007ad070e5f6038ef60000000049454e44ae426082}}{\nonshppict{\pict\picscalex126\picscaley126\piccropl0\piccropr0\piccropt0\piccropb0
+\picw339\pich339\picwgoal192\pichgoal192\wmetafile8\bliptag587145968{\*\blipuid 22ff22f09875d6aa09423248f4f0299b}010009000003c300000000004500000000000400000003010800050000000b0200000000050000000c0211001100030000001e00040000000701040004000000
+070104000800000026060f000600544e5050060145000000410b8600ee0010001000000000001000100000000000280000001000000010000000010001000000
+0000000000000000000000000000000000000000000000000000ffffff000000b32200000000fffc0000fffc00007ffc494cbffeffffdfff0e32ffff0000dfff
+62c5faa80000fffc62c5fff80000ff500100fe00000000000000000000b345000000410bc6008800100010000000000010001000000000002800000010000000
+100000000100010000000000000000000000000000000000000000000000000000000000ffffff00ffff0101ffff010100030101000301018003010140010101
+20000101000001012000000005570000000300000007010100af000001ff0000ffff0000ffff01010800000026060f000600544e50500701040000002701ffff030000000000}}}{\rtlch\fcs1 \af0 \ltrch\fcs0 \insrsid9972925
+\par }{\rtlch\fcs1 \ab\af0 \ltrch\fcs0 \cs30\b\insrsid9972925\charrsid15795588 \hich\af31506\dbch\af31505\loch\f31506 Floppy Disk}{\rtlch\fcs1 \af0 \ltrch\fcs0 \insrsid9972925 \hich\af31506\dbch\af31505\loch\f31506 : }{\field\flddirty{\*\fldinst {\rtlch\fcs1
+\af0 \ltrch\fcs0 \insrsid9972925 \hich\af31506\dbch\af31505\loch\f31506 HYPERLINK "}{\rtlch\fcs1 \af0 \ltrch\fcs0 \insrsid9972925\charrsid15795588 \hich\af31506\dbch\af31505\loch\f31506 https://fontawesome.com/icons/floppy-disk?s=regular}{\rtlch\fcs1
+\af0 \ltrch\fcs0 \insrsid9972925 \hich\af31506\dbch\af31505\loch\f31506 " }{\rtlch\fcs1 \af0 \ltrch\fcs0 \insrsid5193884 {\*\datafield
+00d0c9ea79f9bace118c8200aa004ba90b0200000003000000e0c9ea79f9bace118c8200aa004ba90b80000000680074007400700073003a002f002f0066006f006e00740061007700650073006f006d0065002e0063006f006d002f00690063006f006e0073002f0066006c006f007000700079002d006400690073006b00
+3f0073003d0072006500670075006c00610072000000795881f43b1d7f48af2c825dc485276300000000a5ab0003e6}}}{\fldrslt {\rtlch\fcs1 \af0 \ltrch\fcs0 \cs45\ul\cf22\insrsid9972925\charrsid14120838 \hich\af31506\dbch\af31505\loch\f31506
+https://fontawesome.com/icons/floppy-disk?s=regular}}}\sectd \ltrsect\linex0\endnhere\sectlinegrid360\sectdefaultcl\sftnbj {\rtlch\fcs1 \af0 \ltrch\fcs0 \insrsid9972925 \tab \tab \tab }{\rtlch\fcs1 \af0 \ltrch\fcs0
+\lang1024\langfe1024\noproof\insrsid11485576\charrsid7695206 {\*\shppict{\pict{\*\picprop\shplid1039{\sp{\sn shapeType}{\sv 75}}{\sp{\sn fFlipH}{\sv 0}}{\sp{\sn fFlipV}{\sv 0}}{\sp{\sn fLockRotation}{\sv 0}}{\sp{\sn fLockAspectRatio}{\sv 1}}
+{\sp{\sn fLockPosition}{\sv 0}}{\sp{\sn fLockAgainstSelect}{\sv 0}}{\sp{\sn fLockCropping}{\sv 0}}{\sp{\sn fLockVerticies}{\sv 0}}{\sp{\sn fLockAgainstGrouping}{\sv 0}}{\sp{\sn pictureGray}{\sv 0}}{\sp{\sn pictureBiLevel}{\sv 0}}{\sp{\sn fFilled}{\sv 0}}
+{\sp{\sn fNoFillHitTest}{\sv 0}}{\sp{\sn fLine}{\sv 0}}{\sp{\sn wzName}{\sv Picture 16}}{\sp{\sn dhgt}{\sv 251658240}}{\sp{\sn fHidden}{\sv 0}}{\sp{\sn fLayoutInCell}{\sv 1}}}\picscalex100\picscaley100\piccropl0\piccropr0\piccropt0\piccropb0
+\picw423\pich423\picwgoal240\pichgoal240\pngblip\bliptag1448116714{\*\blipuid 565081eae064143cbbf8c270afe16672}89504e470d0a1a0a0000000d49484452000000100000001008060000001ff3ff61000000d649444154388d95930b0dc2400c401f0b0290701270c0703009c309
+12707073000ec0c186031c6c0e46baf448b94fb2bda4c9adbfb5bd1e400b8cc0bc523c869d061f800fe0800918f8c7a9043ae0123e246b0fd47a7e927255dbdd
+54bb5452a9eb9484e4790367f597d6fd3ee3e6f48f9693390f9aa4d724bfb2eb1543b4892566b615bc74308e32b7d812b7d015430b5479f58237a5fbc46ac85d
+5d93e9bf897c9619942ac8cd21a72bb6f0887663525d422981acb5dcb50489c8597459c22a6f456266fb9864c3d6aeb4f81f83ffd6e71c6404da2ffd4857cc4f3101bd0000000049454e44ae426082}}{\nonshppict{\pict\picscalex126\picscaley126\piccropl0\piccropr0\piccropt0\piccropb0
+\picw339\pich339\picwgoal192\pichgoal192\wmetafile8\bliptag1448116714{\*\blipuid 565081eae064143cbbf8c270afe16672}010009000003c300000000004500000000000400000003010800050000000b0200000000050000000c0211001100030000001e00040000000701040004000000
+070104000800000026060f000600544e5050060145000000410b8600ee0010001000000000001000100000000000280000001000000010000000010001000000
+0000000000000000000000000000000000000000000000000000ffffff007fffb322ffff0000c0010000c3830000c3c1494cc3e3ffffc7c10e32c3c30000c001
+62c5caa30000dff162c5fae30000d0770100f83e0000dd7c0000fff8000045000000410bc6008800100010000000000010001000000000002800000010000000
+100000000100010000000000000000000000000000000000000000000000000000000000ffffff0080000000000000003ffe00003c7c00003c3e00003c1c0000
+383e00003c3c00003ffe0101355c0101200e0101051c01002f88010107c1000022830101000700000800000026060f000600544e50500701040000002701ffff030000000000}}}{\rtlch\fcs1 \af0 \ltrch\fcs0 \insrsid9972925
+\par }{\rtlch\fcs1 \ab\af0 \ltrch\fcs0 \cs30\b\insrsid9972925\charrsid8019431 \hich\af31506\dbch\af31505\loch\f31506 Rotate Left}{\rtlch\fcs1 \af0 \ltrch\fcs0 \insrsid9972925 \hich\af31506\dbch\af31505\loch\f31506 : }{\field\flddirty{\*\fldinst {\rtlch\fcs1
+\af0 \ltrch\fcs0 \insrsid9972925 \hich\af31506\dbch\af31505\loch\f31506 HYPERLINK "}{\rtlch\fcs1 \af0 \ltrch\fcs0 \insrsid9972925\charrsid8019431 \hich\af31506\dbch\af31505\loch\f31506 https://fontawesome.com/icons/rotate-left?s=solid}{\rtlch\fcs1 \af0
+\ltrch\fcs0 \insrsid9972925 \hich\af31506\dbch\af31505\loch\f31506 " }{\rtlch\fcs1 \af0 \ltrch\fcs0 \insrsid5193884 {\*\datafield
+00d0c9ea79f9bace118c8200aa004ba90b0200000003000000e0c9ea79f9bace118c8200aa004ba90b7c000000680074007400700073003a002f002f0066006f006e00740061007700650073006f006d0065002e0063006f006d002f00690063006f006e0073002f0072006f0074006100740065002d006c00650066007400
+3f0073003d0073006f006c00690064000000795881f43b1d7f48af2c825dc485276300000000a5ab0003a1}}}{\fldrslt {\rtlch\fcs1 \af0 \ltrch\fcs0 \cs45\ul\cf22\insrsid9972925\charrsid14120838 \hich\af31506\dbch\af31505\loch\f31506
+https://fontawesome.com/icons/rotate-left?s=solid}}}\sectd \ltrsect\linex0\endnhere\sectlinegrid360\sectdefaultcl\sftnbj {\rtlch\fcs1 \af0 \ltrch\fcs0 \insrsid9972925 \tab \tab \tab \tab }{\rtlch\fcs1 \af0 \ltrch\fcs0
+\lang1024\langfe1024\noproof\insrsid11485576\charrsid7695206 {\*\shppict{\pict{\*\picprop\shplid1038{\sp{\sn shapeType}{\sv 75}}{\sp{\sn fFlipH}{\sv 0}}{\sp{\sn fFlipV}{\sv 0}}{\sp{\sn fLockRotation}{\sv 0}}{\sp{\sn fLockAspectRatio}{\sv 1}}
+{\sp{\sn fLockPosition}{\sv 0}}{\sp{\sn fLockAgainstSelect}{\sv 0}}{\sp{\sn fLockCropping}{\sv 0}}{\sp{\sn fLockVerticies}{\sv 0}}{\sp{\sn fLockAgainstGrouping}{\sv 0}}{\sp{\sn pictureGray}{\sv 0}}{\sp{\sn pictureBiLevel}{\sv 0}}{\sp{\sn fFilled}{\sv 0}}
+{\sp{\sn fNoFillHitTest}{\sv 0}}{\sp{\sn fLine}{\sv 0}}{\sp{\sn wzName}{\sv Picture 7}}{\sp{\sn dhgt}{\sv 251658240}}{\sp{\sn fHidden}{\sv 0}}{\sp{\sn fLayoutInCell}{\sv 1}}}\picscalex100\picscaley100\piccropl0\piccropr0\piccropt0\piccropb0
+\picw423\pich423\picwgoal240\pichgoal240\pngblip\bliptag-890727871{\*\blipuid cae892419c054af4ab1028c5bb3010de}89504e470d0a1a0a0000000d49484452000000100000001008060000001ff3ff61000000ad49444154388d63606060780fc5060c1000a2e743c5fe23e1f30c0c
+0c050c0c0c020c6800a6e83d54e37f0218d93230c0a50164580203038303d4e6f368f20ef80c084077261414a0b918ec1d6c0660381387210db80c00e1fd185a
+1100e69dfb2c0c0c0c8d18d210700043040136425da8802143247080b9948902032802f03020c7108c5840060150415c8183351d2003e4f44f744a440620db49ce0bd84218576e045980e16cf20103030300865f62beb1a50f5a0000000049454e44ae426082}}{\nonshppict
+{\pict\picscalex126\picscaley126\piccropl0\piccropr0\piccropt0\piccropb0\picw339\pich339\picwgoal192\pichgoal192\wmetafile8\bliptag-890727871{\*\blipuid cae892419c054af4ab1028c5bb3010de}
+010009000003c300000000004500000000000400000003010800050000000b0200000000050000000c0211001100030000001e00040000000701040004000000
+070104000800000026060f000600544e5050060145000000410b8600ee0010001000000000001000100000000000280000001000000010000000010001000000
+0000000000000000000000000000000000000000000000000000ffffff000000b3220be000001ff800001ebe0000001e494c000effff00070e32000300000003
+62c5aa030000fc0762c5fe0e0000f81e0100febe0000fff80000efe8000045000000410bc6008800100010000000000010001000000000002800000010000000
+100000000100010000000000000000000000000000000000000000000000000000000000ffffff00ffff0101f41f0101e0070101e1410101ffe10101fff10000
+fff80001fffc0101fffc010055fc000003f8000001f1010107e101000141000100070000101700010800000026060f000600544e50500701040000002701ffff030000000000}}}{\rtlch\fcs1 \af0 \ltrch\fcs0 \insrsid9972925
+\par }{\rtlch\fcs1 \ab\af0 \ltrch\fcs0 \cs30\b\insrsid9972925\charrsid8019431 \hich\af31506\dbch\af31505\loch\f31506 Rotate}{\rtlch\fcs1 \ab\af0 \ltrch\fcs0 \cs30\b\insrsid9972925 \hich\af31506\dbch\af31505\loch\f31506 }{\rtlch\fcs1 \ab\af0 \ltrch\fcs0
+\cs30\b\insrsid9972925\charrsid8019431 \hich\af31506\dbch\af31505\loch\f31506 Right}{\rtlch\fcs1 \af0 \ltrch\fcs0 \insrsid9972925 \hich\af31506\dbch\af31505\loch\f31506 : }{\field\flddirty{\*\fldinst {\rtlch\fcs1 \af0 \ltrch\fcs0 \insrsid9972925
+\hich\af31506\dbch\af31505\loch\f31506 HYPERLINK "}{\rtlch\fcs1 \af0 \ltrch\fcs0 \insrsid9972925\charrsid8019431 \hich\af31506\dbch\af31505\loch\f31506 https://fontawesome.com/icons/rotate-right?s=solid}{\rtlch\fcs1 \af0 \ltrch\fcs0 \insrsid9972925
+\hich\af31506\dbch\af31505\loch\f31506 " }{\rtlch\fcs1 \af0 \ltrch\fcs0 \insrsid5193884 {\*\datafield
+00d0c9ea79f9bace118c8200aa004ba90b0200000003000000e0c9ea79f9bace118c8200aa004ba90b7e000000680074007400700073003a002f002f0066006f006e00740061007700650073006f006d0065002e0063006f006d002f00690063006f006e0073002f0072006f0074006100740065002d007200690067006800
+74003f0073003d0073006f006c00690064000000795881f43b1d7f48af2c825dc485276300000000a5ab000300}}}{\fldrslt {\rtlch\fcs1 \af0 \ltrch\fcs0 \cs45\ul\cf22\insrsid9972925\charrsid14120838 \hich\af31506\dbch\af31505\loch\f31506
+https://fontawesome.com/icons/rotate-right?s=solid}}}\sectd \ltrsect\linex0\endnhere\sectlinegrid360\sectdefaultcl\sftnbj {\rtlch\fcs1 \af0 \ltrch\fcs0 \insrsid9972925 \tab \tab \tab }{\rtlch\fcs1 \af0 \ltrch\fcs0
+\lang1024\langfe1024\noproof\insrsid11485576\charrsid7695206 {\*\shppict{\pict{\*\picprop\shplid1037{\sp{\sn shapeType}{\sv 75}}{\sp{\sn fFlipH}{\sv 0}}{\sp{\sn fFlipV}{\sv 0}}{\sp{\sn fLockRotation}{\sv 0}}{\sp{\sn fLockAspectRatio}{\sv 1}}
+{\sp{\sn fLockPosition}{\sv 0}}{\sp{\sn fLockAgainstSelect}{\sv 0}}{\sp{\sn fLockCropping}{\sv 0}}{\sp{\sn fLockVerticies}{\sv 0}}{\sp{\sn fLockAgainstGrouping}{\sv 0}}{\sp{\sn pictureGray}{\sv 0}}{\sp{\sn pictureBiLevel}{\sv 0}}{\sp{\sn fFilled}{\sv 0}}
+{\sp{\sn fNoFillHitTest}{\sv 0}}{\sp{\sn fLine}{\sv 0}}{\sp{\sn wzName}{\sv Picture 8}}{\sp{\sn dhgt}{\sv 251658240}}{\sp{\sn fHidden}{\sv 0}}{\sp{\sn fLayoutInCell}{\sv 1}}}\picscalex100\picscaley100\piccropl0\piccropr0\piccropt0\piccropb0
+\picw423\pich423\picwgoal240\pichgoal240\pngblip\bliptag-203279650{\*\blipuid f3e232de736bc6718e6a27f804b873a6}89504e470d0a1a0a0000000d49484452000000100000001008060000001ff3ff61000000bf49444154388d9d93c10dc2300c459f8001ca8d6347c8081d811118
+a16cc0269da11370e5d6113a0262029023b7722327447d922f71fe8f9d382434400f4cc0d7c41b1880a0db83ae49acc8e29c08bd1854b8e45647bb79d24a3ae0
+a622cf2c1a3489639ff6a55c1d71347854884372c8c6603665e7783ac21827a055d198110b776dc16571ebbce43f0e26bfcba0f60eb21c81b39e7e013ec02bb73947ed1cb4fae4ee654a05b94914819dc4cdfc5b4ac36243aa28b653fa8dbb5fca07f8012b686105f2ada5540000000049454e44ae426082}}{\nonshppict
+{\pict\picscalex126\picscaley126\piccropl0\piccropr0\piccropt0\piccropb0\picw339\pich339\picwgoal192\pichgoal192\wmetafile8\bliptag-203279650{\*\blipuid f3e232de736bc6718e6a27f804b873a6}
+010009000003c300000000004500000000000400000003010800050000000b0200000000050000000c0211001100030000001e00040000000701040004000000
+070104000800000026060f000600544e5050060145000000410b8600ee0010001000000000001000100000000000280000001000000010000000010001000000
+0000000000000000000000000000000000000000000000000000ffffff000000b3220fe000001ff000003ef800067010494ce000ffffc0000e32e0000000c000
+62c5e02a0000c07f62c5e03f0000701f01003ebf00001ff700000fe300f445000000410bc6008800100010000000000010001000000000002800000010000000
+100000000100010000000000000000000000000000000000000000000000000000000000ffffff00ffff0101f01f0101e00f0101c10701018fef01011fff0000
+3fff00011fff01013fff01001fd500003f8000001fc001018fe00000c1400001e0080000f01c01010800000026060f000600544e50500701040000002701ffff030000000000}}}{\rtlch\fcs1 \af0 \ltrch\fcs0 \insrsid9972925
+\par }{\rtlch\fcs1 \ab\af0 \ltrch\fcs0 \cs30\b\insrsid9972925\charrsid15795588 \hich\af31506\dbch\af31505\loch\f31506 Scissors}{\rtlch\fcs1 \af0 \ltrch\fcs0 \insrsid9972925 \hich\af31506\dbch\af31505\loch\f31506 : }{\field\flddirty{\*\fldinst {\rtlch\fcs1
+\af0 \ltrch\fcs0 \insrsid9972925 \hich\af31506\dbch\af31505\loch\f31506 HYPERLINK "}{\rtlch\fcs1 \af0 \ltrch\fcs0 \insrsid9972925\charrsid15795588 \hich\af31506\dbch\af31505\loch\f31506 https://fontawesome.com/icons/scissors?s=solid}{\rtlch\fcs1 \af0
+\ltrch\fcs0 \insrsid9972925 \hich\af31506\dbch\af31505\loch\f31506 " }{\rtlch\fcs1 \af0 \ltrch\fcs0 \insrsid5193884 {\*\datafield
+00d0c9ea79f9bace118c8200aa004ba90b0200000003000000e0c9ea79f9bace118c8200aa004ba90b76000000680074007400700073003a002f002f0066006f006e00740061007700650073006f006d0065002e0063006f006d002f00690063006f006e0073002f00730063006900730073006f00720073003f0073003d00
+73006f006c00690064000000795881f43b1d7f48af2c825dc485276300000000a5ab000300}}}{\fldrslt {\rtlch\fcs1 \af0 \ltrch\fcs0 \cs45\ul\cf22\insrsid9972925\charrsid14120838 \hich\af31506\dbch\af31505\loch\f31506 https://fontawesome.com/icons/scissors?s=solid}}}
+\sectd \ltrsect\linex0\endnhere\sectlinegrid360\sectdefaultcl\sftnbj {\rtlch\fcs1 \af0 \ltrch\fcs0 \insrsid9972925 \tab \tab \tab \tab \tab }{\rtlch\fcs1 \af0 \ltrch\fcs0 \lang1024\langfe1024\noproof\insrsid11485576\charrsid7695206 {\*\shppict
+{\pict{\*\picprop\shplid1036{\sp{\sn shapeType}{\sv 75}}{\sp{\sn fFlipH}{\sv 0}}{\sp{\sn fFlipV}{\sv 0}}{\sp{\sn fLockRotation}{\sv 0}}{\sp{\sn fLockAspectRatio}{\sv 1}}{\sp{\sn fLockPosition}{\sv 0}}{\sp{\sn fLockAgainstSelect}{\sv 0}}
+{\sp{\sn fLockCropping}{\sv 0}}{\sp{\sn fLockVerticies}{\sv 0}}{\sp{\sn fLockAgainstGrouping}{\sv 0}}{\sp{\sn pictureGray}{\sv 0}}{\sp{\sn pictureBiLevel}{\sv 0}}{\sp{\sn fFilled}{\sv 0}}
+{\sp{\sn fNoFillHitTest}{\sv 0}}{\sp{\sn fLine}{\sv 0}}{\sp{\sn wzName}{\sv Picture 14}}{\sp{\sn dhgt}{\sv 251658240}}{\sp{\sn fHidden}{\sv 0}}{\sp{\sn fLayoutInCell}{\sv 1}}}\picscalex100\picscaley100\piccropl0\piccropr0\piccropt0\piccropb0
+\picw423\pich423\picwgoal240\pichgoal240\pngblip\bliptag1907437281{\*\blipuid 71b12ee1d267632c8abee720ab8ac99f}89504e470d0a1a0a0000000d49484452000000100000001008060000001ff3ff61000000a049444154388dad93d10dc3200c054f9da02330022364a56ed00d32
+0223649474938c40446a24cbc401357d121f80efd9804134031b906594f9135f01881ace276371701dbfa23227714d2a201838994487419d4c12144ed63cf838
+6656154c2630f6e0ab3ba88197f0884917d6aa9b36ab0b3f0c1c1bcbaf3ec0ab591d3cc22a97bb7955f4e068bab431b9fd8c7f6ba4d2fbb682a156befd998ade3f7d676007490688639e1d64650000000049454e44ae426082}}{\nonshppict
+{\pict\picscalex126\picscaley126\piccropl0\piccropr0\piccropt0\piccropb0\picw339\pich339\picwgoal192\pichgoal192\wmetafile8\bliptag1907437281{\*\blipuid 71b12ee1d267632c8abee720ab8ac99f}
+010009000003c300000000004500000000000400000003010800050000000b0200000000050000000c0211001100030000001e00040000000701040004000000
+070104000800000026060f000600544e5050060145000000410b8600ee0010001000000000001000100000000000280000001000000010000000010001000000
+0000000000000000000000000000000000000000000000000000ffffff007c00b322fe0f0000c61f0000ee3e0000c67c494cff78ffff7fb00e3207e0000007c0
+62c53ff000007f7862c5ee7c0000c61c0100ee1f00007c0700007c02000045000000410bc6008800100010000000000010001000000000002800000010000000
+100000000100010000000000000000000000000000000000000000000000000000000000ffffff0083ff000001f0010139e0010111c101013983000000870001
+804f0101f81f0000f83f0101c00f0001808701001183000039e3000111e0000183f8000083fd00010800000026060f000600544e50500701040000002701ffff030000000000}}}{\rtlch\fcs1 \af0 \ltrch\fcs0 \insrsid9972925
+\par }{\rtlch\fcs1 \ab\af0 \ltrch\fcs0 \cs30\b\insrsid9972925\charrsid15795588 \hich\af31506\dbch\af31505\loch\f31506 Copy}{\rtlch\fcs1 \af0 \ltrch\fcs0 \insrsid9972925 \hich\af31506\dbch\af31505\loch\f31506 : }{\field\flddirty{\*\fldinst {\rtlch\fcs1 \af0
+\ltrch\fcs0 \insrsid9972925 \hich\af31506\dbch\af31505\loch\f31506 HYPERLINK "}{\rtlch\fcs1 \af0 \ltrch\fcs0 \insrsid9972925\charrsid15795588 \hich\af31506\dbch\af31505\loch\f31506 https://fontawesome.com/icons/copy?s=solid}{\rtlch\fcs1 \af0 \ltrch\fcs0
+\insrsid9972925 \hich\af31506\dbch\af31505\loch\f31506 " }{\rtlch\fcs1 \af0 \ltrch\fcs0 \insrsid5193884 {\*\datafield
+00d0c9ea79f9bace118c8200aa004ba90b0200000003000000e0c9ea79f9bace118c8200aa004ba90b6e000000680074007400700073003a002f002f0066006f006e00740061007700650073006f006d0065002e0063006f006d002f00690063006f006e0073002f0063006f00700079003f0073003d0073006f006c006900
+64000000795881f43b1d7f48af2c825dc485276300000000a5ab000300}}}{\fldrslt {\rtlch\fcs1 \af0 \ltrch\fcs0 \cs45\ul\cf22\insrsid9972925\charrsid14120838 \hich\af31506\dbch\af31505\loch\f31506 https://fontawesome.com/icons/copy?s=solid}}}\sectd \ltrsect
+\linex0\endnhere\sectlinegrid360\sectdefaultcl\sftnbj {\rtlch\fcs1 \af0 \ltrch\fcs0 \insrsid9972925 \tab \tab \tab \tab \tab }{\rtlch\fcs1 \af0 \ltrch\fcs0 \lang1024\langfe1024\noproof\insrsid11485576\charrsid7695206 {\*\shppict
+{\pict{\*\picprop\shplid1035{\sp{\sn shapeType}{\sv 75}}{\sp{\sn fFlipH}{\sv 0}}{\sp{\sn fFlipV}{\sv 0}}{\sp{\sn fLockRotation}{\sv 0}}{\sp{\sn fLockAspectRatio}{\sv 1}}{\sp{\sn fLockPosition}{\sv 0}}{\sp{\sn fLockAgainstSelect}{\sv 0}}
+{\sp{\sn fLockCropping}{\sv 0}}{\sp{\sn fLockVerticies}{\sv 0}}{\sp{\sn fLockAgainstGrouping}{\sv 0}}{\sp{\sn pictureGray}{\sv 0}}{\sp{\sn pictureBiLevel}{\sv 0}}{\sp{\sn fFilled}{\sv 0}}
+{\sp{\sn fNoFillHitTest}{\sv 0}}{\sp{\sn fLine}{\sv 0}}{\sp{\sn wzName}{\sv Picture 17}}{\sp{\sn dhgt}{\sv 251658240}}{\sp{\sn fHidden}{\sv 0}}{\sp{\sn fLayoutInCell}{\sv 1}}}\picscalex100\picscaley100\piccropl0\piccropr0\piccropt0\piccropb0
+\picw423\pich423\picwgoal240\pichgoal240\pngblip\bliptag-559363706{\*\blipuid dea8c9864ddaca735bfa31facd406145}89504e470d0a1a0a0000000d49484452000000100000001008060000001ff3ff610000007349444154388ded92cb0d802010449fc4022cc512b4022dc1ce2c49
+3b826020227ff4ea4bf6b2ec0c4c580a1c80f46a02f6bcecc6175b03694dfa4052cff6d6600166b711cbe93e355b0218833b1a105fc4fc064583013883ee93eb3cf5cf7a3f56b30f493a335c42cf3547a8421b54e58c02283ecc286925daaafd0000000049454e44ae426082}}{\nonshppict
+{\pict\picscalex126\picscaley126\piccropl0\piccropr0\piccropt0\piccropb0\picw339\pich339\picwgoal192\pichgoal192\wmetafile8\bliptag-559363706{\*\blipuid dea8c9864ddaca735bfa31facd406145}
+010009000003c300000000004500000000000400000003010800050000000b0200000000050000000c0211001100030000001e00040000000701040004000000
+070104000800000026060f000600544e5050060145000000410b8600ee0010001000000000001000100000000000280000001000000010000000010001000000
+0000000000000000000000000000000000000000000000000000ffffff007f00b322ff800000ff800000fe800000fdff494cfffffffffdff0e32ffff0000fdff
+62c5ffff0000fdff62c5ffff000001f0010001ff000001f4000001fc000045000000410bc6008800100010000000000010001000000000002800000010000000
+100000000100010000000000000000000000000000000000000000000000000000000000ffffff0080ff0000007f0000007f0101017f01010200000000000000
+020001010000010102000000000000000200010100000101fe0f0000fe000001fe0b0101fe0301010800000026060f000600544e50500701040000002701ffff030000000000}}}{\rtlch\fcs1 \af0 \ltrch\fcs0 \insrsid9972925
+\par }{\rtlch\fcs1 \ab\af0 \ltrch\fcs0 \cs30\b\lang2058\langfe1033\langnp2058\insrsid9972925\charrsid4869634 \hich\af31506\dbch\af31505\loch\f31506 Paste}{\rtlch\fcs1 \af0 \ltrch\fcs0 \lang2058\langfe1033\langnp2058\insrsid9972925\charrsid4869634
+\hich\af31506\dbch\af31505\loch\f31506 : }{\field\flddirty{\*\fldinst {\rtlch\fcs1 \af0 \ltrch\fcs0 \lang2058\langfe1033\langnp2058\insrsid9972925\charrsid4869634 \hich\af31506\dbch\af31505\loch\f31506 HYPERLINK "\hich\af31506\dbch\af31505\loch\f31506
+https://fontawesome.com/icons/paste?s=solid\hich\af31506\dbch\af31505\loch\f31506 " }{\rtlch\fcs1 \af0 \ltrch\fcs0 \insrsid5193884 {\*\datafield
+00d0c9ea79f9bace118c8200aa004ba90b0200000003000000e0c9ea79f9bace118c8200aa004ba90b70000000680074007400700073003a002f002f0066006f006e00740061007700650073006f006d0065002e0063006f006d002f00690063006f006e0073002f00700061007300740065003f0073003d0073006f006c00
+690064000000795881f43b1d7f48af2c825dc485276300000000a5ab000300}}}{\fldrslt {\rtlch\fcs1 \af0 \ltrch\fcs0 \cs45\ul\cf22\lang2058\langfe1033\langnp2058\insrsid9972925\charrsid4869634 \hich\af31506\dbch\af31505\loch\f31506
+https://fontawesome.com/icons/paste?s=solid}}}\sectd \ltrsect\linex0\endnhere\sectlinegrid360\sectdefaultcl\sftnbj {\rtlch\fcs1 \af0 \ltrch\fcs0 \lang2058\langfe1033\langnp2058\insrsid9972925\charrsid4869634 \tab \tab \tab \tab \tab }{\rtlch\fcs1 \af0
+\ltrch\fcs0 \lang1024\langfe1024\noproof\insrsid11485576\charrsid7695206 {\*\shppict{\pict{\*\picprop\shplid1034{\sp{\sn shapeType}{\sv 75}}{\sp{\sn fFlipH}{\sv 0}}{\sp{\sn fFlipV}{\sv 0}}{\sp{\sn fLockRotation}{\sv 0}}{\sp{\sn fLockAspectRatio}{\sv 1}}
+{\sp{\sn fLockPosition}{\sv 0}}{\sp{\sn fLockAgainstSelect}{\sv 0}}{\sp{\sn fLockCropping}{\sv 0}}{\sp{\sn fLockVerticies}{\sv 0}}{\sp{\sn fLockAgainstGrouping}{\sv 0}}{\sp{\sn pictureGray}{\sv 0}}{\sp{\sn pictureBiLevel}{\sv 0}}{\sp{\sn fFilled}{\sv 0}}
+{\sp{\sn fNoFillHitTest}{\sv 0}}{\sp{\sn fLine}{\sv 0}}{\sp{\sn wzName}{\sv Picture 18}}{\sp{\sn dhgt}{\sv 251658240}}{\sp{\sn fHidden}{\sv 0}}{\sp{\sn fLayoutInCell}{\sv 1}}}\picscalex100\picscaley100\piccropl0\piccropr0\piccropt0\piccropb0
+\picw423\pich423\picwgoal240\pichgoal240\pngblip\bliptag1375566707{\*\blipuid 51fd7b73457fca82c5431639d08c2a9f}89504e470d0a1a0a0000000d49484452000000100000001008060000001ff3ff610000008549444154388ddd92cb0d802010449fc6822c012bb0054bb004acc4
+522c414bb1030c0907645df073f3251392617760092458c005d97453638d9abc96a0d85b955e6aa015ae44ad69840326ac3b30019ba848708ac68bf15c38608e23b40073e1c5feac8d7097e16d400f745f4638a916f90ff94940f1a7e5a80afbfea58b37c891bf1d6c07a6df3bc83a90323d0000000049454e44ae426082}
+}{\nonshppict{\pict\picscalex126\picscaley126\piccropl0\piccropr0\piccropt0\piccropb0\picw339\pich339\picwgoal192\pichgoal192\wmetafile8\bliptag1375566707{\*\blipuid 51fd7b73457fca82c5431639d08c2a9f}
+010009000003c300000000004500000000000400000003010800050000000b0200000000050000000c0211001100030000001e00040000000701040004000000
+070104000800000026060f000600544e5050060145000000410b8600ee0010001000000000001000100000000000280000001000000010000000010001000000
+0000000000000000000000000000000000000000000000000000ffffff0001ffb32201ff00007dff0000ffff0000fdff494cfffffffffdff0e32ffff0000fdf0
+62c5ffff0000fdf462c5fffc0000ff000100ffc00000f7c000000e00000045000000410bc6008800100010000000000010001000000000002800000010000000
+100000000100010000000000000000000000000000000000000000000000000000000000ffffff00fe000101fe00010082000000000000000200010100000100
+0200000000000000020f000000000100020b00000003000000ff0000003f0000083f0000f1ff00000800000026060f000600544e50500701040000002701ffff030000000000}}}{\rtlch\fcs1 \af0 \ltrch\fcs0 \lang2058\langfe1033\langnp2058\insrsid9972925\charrsid4869634
+\par }{\rtlch\fcs1 \ab\af0 \ltrch\fcs0 \cs30\b\insrsid9972925\charrsid16348870 \hich\af31506\dbch\af31505\loch\f31506 Map}{\rtlch\fcs1 \af0 \ltrch\fcs0 \insrsid9972925 \hich\af31506\dbch\af31505\loch\f31506 : }{\field\flddirty{\*\fldinst {\rtlch\fcs1 \af0
+\ltrch\fcs0 \insrsid9972925 \hich\af31506\dbch\af31505\loch\f31506 HYPERLINK "}{\rtlch\fcs1 \af0 \ltrch\fcs0 \insrsid9972925\charrsid16348870 \hich\af31506\dbch\af31505\loch\f31506 https://fontawesome.com/icons/map?s=regular}{\rtlch\fcs1 \af0
+\ltrch\fcs0 \insrsid9972925 \hich\af31506\dbch\af31505\loch\f31506 " }{\rtlch\fcs1 \af0 \ltrch\fcs0 \insrsid5193884 {\*\datafield
+00d0c9ea79f9bace118c8200aa004ba90b0200000003000000e0c9ea79f9bace118c8200aa004ba90b70000000680074007400700073003a002f002f0066006f006e00740061007700650073006f006d0065002e0063006f006d002f00690063006f006e0073002f006d00610070003f0073003d0072006500670075006c00
+610072000000795881f43b1d7f48af2c825dc485276300000000a5ab000300}}}{\fldrslt {\rtlch\fcs1 \af0 \ltrch\fcs0 \cs45\ul\cf22\insrsid9972925\charrsid14120838 \hich\af31506\dbch\af31505\loch\f31506 https://fontawesome.com/icons/map?s=regular}}}\sectd \ltrsect
+\linex0\endnhere\sectlinegrid360\sectdefaultcl\sftnbj {\rtlch\fcs1 \af0 \ltrch\fcs0 \insrsid9972925 \tab \tab \tab \tab \tab }{\rtlch\fcs1 \af0 \ltrch\fcs0 \lang1024\langfe1024\noproof\insrsid11485576\charrsid7695206 {\*\shppict
+{\pict{\*\picprop\shplid1033{\sp{\sn shapeType}{\sv 75}}{\sp{\sn fFlipH}{\sv 0}}{\sp{\sn fFlipV}{\sv 0}}{\sp{\sn fLockRotation}{\sv 0}}{\sp{\sn fLockAspectRatio}{\sv 1}}{\sp{\sn fLockPosition}{\sv 0}}{\sp{\sn fLockAgainstSelect}{\sv 0}}
+{\sp{\sn fLockCropping}{\sv 0}}{\sp{\sn fLockVerticies}{\sv 0}}{\sp{\sn fLockAgainstGrouping}{\sv 0}}{\sp{\sn pictureGray}{\sv 0}}{\sp{\sn pictureBiLevel}{\sv 0}}{\sp{\sn fFilled}{\sv 0}}
+{\sp{\sn fNoFillHitTest}{\sv 0}}{\sp{\sn fLine}{\sv 0}}{\sp{\sn wzName}{\sv Picture 20}}{\sp{\sn dhgt}{\sv 251658240}}{\sp{\sn fHidden}{\sv 0}}{\sp{\sn fLayoutInCell}{\sv 1}}}\picscalex100\picscaley100\piccropl0\piccropr0\piccropt0\piccropb0
+\picw423\pich423\picwgoal240\pichgoal240\pngblip\bliptag-905517294{\*\blipuid ca06e71200a600ceeb6f51bfe4c35638}89504e470d0a1a0a0000000d49484452000000100000001008060000001ff3ff610000008d49444154388ded925b0d80300c002f04214840c224e0000b48c0c1
+34e0040748400212464abaa46bd8171ff041933dba5cb7253d3e151d10811d48c0064c40ef3ee9b90bda349171b835291c2b5c01c86bb39e05cd6de11d774db3
+f9a2bd2047a871cdd326fc17406bf6d29a1118349776adc0e26a3c5748910db4a6d5e4caa6168288a6fe356b6a8d7b2b801300b53dd38e07c2100000000049454e44ae426082}}{\nonshppict{\pict\picscalex126\picscaley126\piccropl0\piccropr0\piccropt0\piccropb0
+\picw339\pich339\picwgoal192\pichgoal192\wmetafile8\bliptag-905517294{\*\blipuid ca06e71200a600ceeb6f51bfe4c35638}010009000003c300000000004500000000000400000003010800050000000b0200000000050000000c0211001100030000001e00040000000701040004000000
+070104000800000026060f000600544e5050060145000000410b8600ee0010001000000000001000100000000000280000001000000010000000010001000000
+0000000000000000000000000000000000000000000000000000ffffff000000b32200000000c0700000fffe0000d537494c8e23ffffc4310e328e230000c431
+62c58e230000c43162c5eefb000077df01002e030000000000000000000045000000410bc6008800100010000000000010001000000000002800000010000000
+100000000100010000000000000000000000000000000000000000000000000000000000ffffff00ffff0101ffff01013f8f0101000101012ac8010171dc0101
+3bce010171dc01013bce010171dc01013bce00001104010188200000d1fc0000ffff0000ffff00010800000026060f000600544e50500701040000002701ffff030000000000}}}{\rtlch\fcs1 \af0 \ltrch\fcs0 \insrsid9972925
+\par }{\rtlch\fcs1 \ab\af0 \ltrch\fcs0 \cs30\b\insrsid9972925\charrsid16348870 \hich\af31506\dbch\af31505\loch\f31506 Folder Tree}{\rtlch\fcs1 \af0 \ltrch\fcs0 \insrsid9972925 \hich\af31506\dbch\af31505\loch\f31506 : }{\field\flddirty{\*\fldinst {\rtlch\fcs1
+\af0 \ltrch\fcs0 \insrsid9972925 \hich\af31506\dbch\af31505\loch\f31506 HYPERLINK "}{\rtlch\fcs1 \af0 \ltrch\fcs0 \insrsid9972925\charrsid16348870 \hich\af31506\dbch\af31505\loch\f31506 https://fontawesome.com/icons/folder-tree?s=solid}{\rtlch\fcs1 \af0
+\ltrch\fcs0 \insrsid9972925 \hich\af31506\dbch\af31505\loch\f31506 " }{\rtlch\fcs1 \af0 \ltrch\fcs0 \insrsid5193884 {\*\datafield
+00d0c9ea79f9bace118c8200aa004ba90b0200000003000000e0c9ea79f9bace118c8200aa004ba90b7c000000680074007400700073003a002f002f0066006f006e00740061007700650073006f006d0065002e0063006f006d002f00690063006f006e0073002f0066006f006c006400650072002d007400720065006500
+3f0073003d0073006f006c00690064000000795881f43b1d7f48af2c825dc485276300000000a5ab000300}}}{\fldrslt {\rtlch\fcs1 \af0 \ltrch\fcs0 \cs45\ul\cf22\insrsid9972925\charrsid14120838 \hich\af31506\dbch\af31505\loch\f31506
+https://fontawesome.com/icons/folder-tree?s=solid}}}\sectd \ltrsect\linex0\endnhere\sectlinegrid360\sectdefaultcl\sftnbj {\rtlch\fcs1 \af0 \ltrch\fcs0 \insrsid9972925 \tab \tab \tab \tab }{\rtlch\fcs1 \af0 \ltrch\fcs0
+\lang1024\langfe1024\noproof\insrsid11485576\charrsid7695206 {\*\shppict{\pict{\*\picprop\shplid1032{\sp{\sn shapeType}{\sv 75}}{\sp{\sn fFlipH}{\sv 0}}{\sp{\sn fFlipV}{\sv 0}}{\sp{\sn fLockRotation}{\sv 0}}{\sp{\sn fLockAspectRatio}{\sv 1}}
+{\sp{\sn fLockPosition}{\sv 0}}{\sp{\sn fLockAgainstSelect}{\sv 0}}{\sp{\sn fLockCropping}{\sv 0}}{\sp{\sn fLockVerticies}{\sv 0}}{\sp{\sn fLockAgainstGrouping}{\sv 0}}{\sp{\sn pictureGray}{\sv 0}}{\sp{\sn pictureBiLevel}{\sv 0}}{\sp{\sn fFilled}{\sv 0}}
+{\sp{\sn fNoFillHitTest}{\sv 0}}{\sp{\sn fLine}{\sv 0}}{\sp{\sn wzName}{\sv Picture 19}}{\sp{\sn dhgt}{\sv 251658240}}{\sp{\sn fHidden}{\sv 0}}{\sp{\sn fLayoutInCell}{\sv 1}}}\picscalex100\picscaley100\piccropl0\piccropr0\piccropt0\piccropb0
+\picw423\pich423\picwgoal240\pichgoal240\pngblip\bliptag-623945836{\*\blipuid dacf5794f66287628265a377b2f0e2b1}89504e470d0a1a0a0000000d49484452000000100000001008060000001ff3ff610000009149444154388d63a01430323030fc676060f8c0c0c02088c3acf70c
+0c0c0250f6050606864224b90f300318a0866103ffb188c1010b12bb014396816101860816f01f0f76c02307c6c82e68c4349be10186081aa06a18e0028648b1
+800e3e10630032b8008d723820d50b20033622f10f50350cf663c8a2a63a9c80e274802f94417ea6188032132e1780e428000c0c0c0045593d781d547de70000000049454e44ae426082}}{\nonshppict{\pict\picscalex126\picscaley126\piccropl0\piccropr0\piccropt0\piccropb0
+\picw339\pich339\picwgoal192\pichgoal192\wmetafile8\bliptag-623945836{\*\blipuid dacf5794f66287628265a377b2f0e2b1}010009000003c300000000004500000000000400000003010800050000000b0200000000050000000c0211001100030000001e00040000000701040004000000
+070104000800000026060f000600544e5050060145000000410b8600ee0010001000000000001000100000000000280000001000000010000000010001000000
+0000000000000000000000000000000000000000000000000000ffffff000000b32200ff000000ff0000feff0000d4ff494cc0ffffffc0f40e32c0a00000c000
+62c5c0ff0000d4ff62c5feff0000d4ff0100c0ff0000c0f000000000000045000000410bc6008800100010000000000010001000000000002800000010000000
+100000000100010000000000000000000000000000000000000000000000000000000000ffffff00ffff0101ff000101ff000101010001012b0001013f000101
+3f0b00003f5f00003fff01013f0001012b000000010000002b0000003f0000013f0f0000ffff00000800000026060f000600544e50500701040000002701ffff030000000000}}}{\rtlch\fcs1 \af0 \ltrch\fcs0 \insrsid9972925
+\par }\pard \ltrpar\ql \li0\ri0\widctlpar\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0\pararsid6830075 {\rtlch\fcs1 \ab\af0 \ltrch\fcs0 \cs30\b\insrsid6830075\charrsid15275006 \hich\af31506\dbch\af31505\loch\f31506 Plus}{\rtlch\fcs1 \af0
+\ltrch\fcs0 \insrsid6830075 \hich\af31506\dbch\af31505\loch\f31506 : }{\field{\*\fldinst {\rtlch\fcs1 \af0 \ltrch\fcs0 \insrsid6830075 \hich\af31506\dbch\af31505\loch\f31506 HYPERLINK "}{\rtlch\fcs1 \af0 \ltrch\fcs0 \insrsid6830075\charrsid6830075
+\hich\af31506\dbch\af31505\loch\f31506 https://fontawesome.com/icons/plus?s=solid}{\rtlch\fcs1 \af0 \ltrch\fcs0 \insrsid6830075 \hich\af31506\dbch\af31505\loch\f31506 " }{\rtlch\fcs1 \af0 \ltrch\fcs0 \insrsid13263836 {\*\datafield
+00d0c9ea79f9bace118c8200aa004ba90b0200000003000000e0c9ea79f9bace118c8200aa004ba90b6e000000680074007400700073003a002f002f0066006f006e00740061007700650073006f006d0065002e0063006f006d002f00690063006f006e0073002f0070006c00750073003f0073003d0073006f006c006900
+64000000795881f43b1d7f48af2c825dc485276300000000a5ab000300006132}}}{\fldrslt {\rtlch\fcs1 \af0 \ltrch\fcs0 \cs45\ul\cf22\insrsid6830075\charrsid1798261 \hich\af31506\dbch\af31505\loch\f31506 https://fontawesome.com/icons/plus?s=solid}}}\sectd \ltrsect
+\linex0\endnhere\sectlinegrid360\sectdefaultcl\sftnbj {\rtlch\fcs1 \af0 \ltrch\fcs0 \insrsid15275006 \tab \tab \tab \tab \tab \tab }{\rtlch\fcs1 \af0 \ltrch\fcs0 \lang1024\langfe1024\noproof\insrsid11485576\charrsid7695206 {\*\shppict
+{\pict{\*\picprop\shplid1031{\sp{\sn shapeType}{\sv 75}}{\sp{\sn fFlipH}{\sv 0}}{\sp{\sn fFlipV}{\sv 0}}{\sp{\sn fLockRotation}{\sv 0}}{\sp{\sn fLockAspectRatio}{\sv 1}}{\sp{\sn fLockPosition}{\sv 0}}{\sp{\sn fLockAgainstSelect}{\sv 0}}
+{\sp{\sn fLockCropping}{\sv 0}}{\sp{\sn fLockVerticies}{\sv 0}}{\sp{\sn fLockAgainstGrouping}{\sv 0}}{\sp{\sn pictureGray}{\sv 0}}{\sp{\sn pictureBiLevel}{\sv 0}}{\sp{\sn fFilled}{\sv 0}}
+{\sp{\sn fNoFillHitTest}{\sv 0}}{\sp{\sn fLine}{\sv 0}}{\sp{\sn wzName}{\sv Picture 2}}{\sp{\sn dhgt}{\sv 251658240}}{\sp{\sn fHidden}{\sv 0}}{\sp{\sn fLayoutInCell}{\sv 1}}}\picscalex100\picscaley100\piccropl0\piccropr0\piccropt0\piccropb0
+\picw423\pich423\picwgoal240\pichgoal240\pngblip\bliptag-716857488{\*\blipuid d5459f701fe35a755c72d6218d2d7c6e}89504e470d0a1a0a0000000d49484452000000100000001008060000001ff3ff610000006749444154388d63c00314181818de43b1022e654c18220800d22400
+c564194014180606b04003085b2019e0602383070cd068fa4f267e4fb117180978a11fca2e646060b880a102ea055cc001c95b0e38d48cf8840402a084840b8042f80354127b68333030000058ed1c3a27c5de1b0000000049454e44ae426082}}{\nonshppict
+{\pict\picscalex126\picscaley126\piccropl0\piccropr0\piccropt0\piccropb0\picw339\pich339\picwgoal192\pichgoal192\wmetafile8\bliptag-716857488{\*\blipuid d5459f701fe35a755c72d6218d2d7c6e}
+010009000003c300000000004500000000000400000003010800050000000b0200000000050000000c0211001100030000001e00040000000701040004000000
+070104000800000026060f000600544e5050060145000000410b8600ee0010001000000000001000100000000000280000001000000010000000010001000000
+0000000000000000000000000000000000000000000000000000ffffff000100b3220380000001800000038000000180494c0380ffff01c00e32ffff00007fff
+62c52baa0000018062c5038000000180010003800000018000000180000045000000410bc6008800100010000000000010001000000000002800000010000000
+100000000100010000000000000000000000000000000000000000000000000000000000ffffff00feff0101fc7f0100fe7f0101fc7f0101fe7f0101fc7f0000
+fe3f01010000010180000101d4550100fe7f0101fc7f0101fe7f0101fc7f0000fe7f0101fe7f01010800000026060f000600544e50500701040000002701ffff030000000000}}}{\rtlch\fcs1 \af0 \ltrch\fcs0 \insrsid6830075
+\par }{\rtlch\fcs1 \ab\af0 \ltrch\fcs0 \cs30\b\insrsid15275006\charrsid15275006 \hich\af31506\dbch\af31505\loch\f31506 Minus}{\rtlch\fcs1 \af0 \ltrch\fcs0 \insrsid15275006 \hich\af31506\dbch\af31505\loch\f31506 : }{\field{\*\fldinst {\rtlch\fcs1 \af0
+\ltrch\fcs0 \insrsid15275006 \hich\af31506\dbch\af31505\loch\f31506 HYPERLINK "}{\rtlch\fcs1 \af0 \ltrch\fcs0 \insrsid15275006\charrsid15275006 \hich\af31506\dbch\af31505\loch\f31506 https://fontawesome.com/icons/minus?s=solid}{\rtlch\fcs1 \af0
+\ltrch\fcs0 \insrsid15275006 \hich\af31506\dbch\af31505\loch\f31506 " }{\rtlch\fcs1 \af0 \ltrch\fcs0 \insrsid7748460 {\*\datafield
+00d0c9ea79f9bace118c8200aa004ba90b0200000003000000e0c9ea79f9bace118c8200aa004ba90b70000000680074007400700073003a002f002f0066006f006e00740061007700650073006f006d0065002e0063006f006d002f00690063006f006e0073002f006d0069006e00750073003f0073003d0073006f006c00
+690064000000795881f43b1d7f48af2c825dc485276300000000a5ab00036126}}}{\fldrslt {\rtlch\fcs1 \af0 \ltrch\fcs0 \cs45\ul\cf22\insrsid15275006\charrsid8979329 \hich\af31506\dbch\af31505\loch\f31506 https://fontawesome.com/icons/minus?s=solid}}}\sectd \ltrsect
+\linex0\endnhere\sectlinegrid360\sectdefaultcl\sftnbj {\rtlch\fcs1 \af0 \ltrch\fcs0 \insrsid15275006 \tab \tab \tab \tab \tab }{\rtlch\fcs1 \af0 \ltrch\fcs0 \lang1024\langfe1024\noproof\insrsid11485576\charrsid7695206 {\*\shppict
+{\pict{\*\picprop\shplid1030{\sp{\sn shapeType}{\sv 75}}{\sp{\sn fFlipH}{\sv 0}}{\sp{\sn fFlipV}{\sv 0}}{\sp{\sn fLockRotation}{\sv 0}}{\sp{\sn fLockAspectRatio}{\sv 1}}{\sp{\sn fLockPosition}{\sv 0}}{\sp{\sn fLockAgainstSelect}{\sv 0}}
+{\sp{\sn fLockCropping}{\sv 0}}{\sp{\sn fLockVerticies}{\sv 0}}{\sp{\sn fLockAgainstGrouping}{\sv 0}}{\sp{\sn pictureGray}{\sv 0}}{\sp{\sn pictureBiLevel}{\sv 0}}{\sp{\sn fFilled}{\sv 0}}
+{\sp{\sn fNoFillHitTest}{\sv 0}}{\sp{\sn fLine}{\sv 0}}{\sp{\sn wzName}{\sv Picture 2}}{\sp{\sn dhgt}{\sv 251658240}}{\sp{\sn fHidden}{\sv 0}}{\sp{\sn fLayoutInCell}{\sv 1}}}\picscalex100\picscaley100\piccropl0\piccropr0\piccropt0\piccropb0
+\picw423\pich423\picwgoal240\pichgoal240\pngblip\bliptag1904741020{\*\blipuid 71880a9cd3a58a0d21bd096107e7c12f}89504e470d0a1a0a0000000d49484452000000100000001008060000001ff3ff610000001f49444154388d631805c305bc676060f84f267ecf349a0c463c6060
+6060000012ce0fd3cdff4d9b0000000049454e44ae426082}}{\nonshppict{\pict\picscalex126\picscaley126\piccropl0\piccropr0\piccropt0\piccropb0\picw339\pich339\picwgoal192\pichgoal192\wmetafile8\bliptag1904741020{\*\blipuid 71880a9cd3a58a0d21bd096107e7c12f}
+010009000003c300000000004500000000000400000003010800050000000b0200000000050000000c0211001100030000001e00040000000701040004000000
+070104000800000026060f000600544e5050060145000000410b8600ee0010001000000000001000100000000000280000001000000010000000010001000000
+0000000000000000000000000000000000000000000000000000ffffff000000b3220000000000000000000000000000494c0000ffff00000e32ffff00007fff
+62c500000000000062c5000000000000010000000000000000000000000045000000410bc6008800100010000000000010001000000000002800000010000000
+100000000100010000000000000000000000000000000000000000000000000000000000ffffff00ffff0101ffff0101ffff0101ffff0101ffff0101ffff0101
+ffff01010000010180000101ffff0101ffff0101ffff0101ffff0101ffff0101ffff0101ffff01010800000026060f000600544e50500701040000002701ffff030000000000}}}{\rtlch\fcs1 \af0 \ltrch\fcs0 \insrsid15275006
+\par }{\rtlch\fcs1 \ab\af0 \ltrch\fcs0 \cs30\b\lang2058\langfe1033\langnp2058\insrsid15275006\charrsid4869634 \hich\af31506\dbch\af31505\loch\f31506 Trash}{\rtlch\fcs1 \af0 \ltrch\fcs0 \lang2058\langfe1033\langnp2058\insrsid15275006\charrsid4869634
+\hich\af31506\dbch\af31505\loch\f31506 : }{\field{\*\fldinst {\rtlch\fcs1 \af0 \ltrch\fcs0 \lang2058\langfe1033\langnp2058\insrsid15275006\charrsid4869634 \hich\af31506\dbch\af31505\loch\f31506 HYPERLINK "\hich\af31506\dbch\af31505\loch\f31506
+https://fontawesome.com/icons/trash?s=solid\hich\af31506\dbch\af31505\loch\f31506 " }{\rtlch\fcs1 \af0 \ltrch\fcs0 \insrsid7748460 {\*\datafield
+00d0c9ea79f9bace118c8200aa004ba90b0200000003000000e0c9ea79f9bace118c8200aa004ba90b70000000680074007400700073003a002f002f0066006f006e00740061007700650073006f006d0065002e0063006f006d002f00690063006f006e0073002f00740072006100730068003f0073003d0073006f006c00
+690064000000795881f43b1d7f48af2c825dc485276300000000a5ab0003ff30}}}{\fldrslt {\rtlch\fcs1 \af0 \ltrch\fcs0 \cs45\ul\cf22\lang2058\langfe1033\langnp2058\insrsid15275006\charrsid4869634 \hich\af31506\dbch\af31505\loch\f31506
+https://fontawesome.com/icons/trash?s=solid}}}\sectd \ltrsect\linex0\endnhere\sectlinegrid360\sectdefaultcl\sftnbj {\rtlch\fcs1 \af0 \ltrch\fcs0 \lang2058\langfe1033\langnp2058\insrsid15275006\charrsid4869634 \tab \tab \tab \tab \tab }{\rtlch\fcs1 \af0
+\ltrch\fcs0 \lang1024\langfe1024\noproof\insrsid11485576\charrsid7695206 {\*\shppict{\pict{\*\picprop\shplid1029{\sp{\sn shapeType}{\sv 75}}{\sp{\sn fFlipH}{\sv 0}}{\sp{\sn fFlipV}{\sv 0}}{\sp{\sn fLockRotation}{\sv 0}}{\sp{\sn fLockAspectRatio}{\sv 1}}
+{\sp{\sn fLockPosition}{\sv 0}}{\sp{\sn fLockAgainstSelect}{\sv 0}}{\sp{\sn fLockCropping}{\sv 0}}{\sp{\sn fLockVerticies}{\sv 0}}{\sp{\sn fLockAgainstGrouping}{\sv 0}}{\sp{\sn pictureGray}{\sv 0}}{\sp{\sn pictureBiLevel}{\sv 0}}{\sp{\sn fFilled}{\sv 0}}
+{\sp{\sn fNoFillHitTest}{\sv 0}}{\sp{\sn fLine}{\sv 0}}{\sp{\sn wzName}{\sv Picture 3}}{\sp{\sn dhgt}{\sv 251658240}}{\sp{\sn fHidden}{\sv 0}}{\sp{\sn fLayoutInCell}{\sv 1}}}\picscalex100\picscaley100\piccropl0\piccropr0\piccropt0\piccropb0
+\picw423\pich423\picwgoal240\pichgoal240\pngblip\bliptag-43803266{\*\blipuid fd639d7ecc9e47181642eab6df6c275b}89504e470d0a1a0a0000000d49484452000000100000001008060000001ff3ff610000007249444154388d63c001ee333030fc47c3f7b12b8580f7583410c220
+3d0c4c1846910818a1ca051818180c48d47b818181e1038c2340c0b9d83018302299f81fc30efc80912a614055032e60c8e20607b019f001a7723c607085c14112f4c1d50ebc1790810291b912a406a49681818181010095e030bff0661a490000000049454e44ae426082}}{\nonshppict
+{\pict\picscalex126\picscaley126\piccropl0\piccropr0\piccropt0\piccropb0\picw339\pich339\picwgoal192\pichgoal192\wmetafile8\bliptag-43803266{\*\blipuid fd639d7ecc9e47181642eab6df6c275b}
+010009000003c300000000004500000000000400000003010800050000000b0200000000050000000c0211001100030000001e00040000000701040004000000
+070104000800000026060f000600544e5050060145000000410b8600ee0010001000000000001000100000000000280000001000000010000000010001000000
+0000000000000000000000000000000000000000000000000000ffffff001ff0b3223ff800001ffc00003ff8803f1ffc494c3ffcffff1ffc0e323ffc00003ffc
+62c53ffc00003ffc62c53ffc0000000001007ffe00007ffe000007e0ffff45000000410bc6008800100010000000000010001000000000002800000010000000
+100000000100010000000000000000000000000000000000000000000000000000000000ffffff00e00f0100c0070000e0030000c0070101e0030000c0030000
+e0030000c0030101c0030100c0030000c0030000c0030101ffff00008001000080010000f81f01010800000026060f000600544e50500701040000002701ffff030000000000}}}{\rtlch\fcs1 \af0 \ltrch\fcs0 \lang2058\langfe1033\langnp2058\insrsid15275006\charrsid4869634
+\par }{\rtlch\fcs1 \ab\af0 \ltrch\fcs0 \cs30\b\insrsid15275006\charrsid15275006 \hich\af31506\dbch\af31505\loch\f31506 Angle Down}{\rtlch\fcs1 \af0 \ltrch\fcs0 \insrsid15275006 \hich\af31506\dbch\af31505\loch\f31506 : }{\field{\*\fldinst {\rtlch\fcs1 \af0
+\ltrch\fcs0 \insrsid15275006 \hich\af31506\dbch\af31505\loch\f31506 HYPERLINK "}{\rtlch\fcs1 \af0 \ltrch\fcs0 \insrsid15275006\charrsid15275006 \hich\af31506\dbch\af31505\loch\f31506 https://fontawesome.com/icons/angle-down?s=solid}{\rtlch\fcs1 \af0
+\ltrch\fcs0 \insrsid15275006 \hich\af31506\dbch\af31505\loch\f31506 " }{\rtlch\fcs1 \af0 \ltrch\fcs0 \insrsid7748460 {\*\datafield
+00d0c9ea79f9bace118c8200aa004ba90b0200000003000000e0c9ea79f9bace118c8200aa004ba90b7a000000680074007400700073003a002f002f0066006f006e00740061007700650073006f006d0065002e0063006f006d002f00690063006f006e0073002f0061006e0067006c0065002d0064006f0077006e003f00
+73003d0073006f006c00690064000000795881f43b1d7f48af2c825dc485276300000000a5ab00030072}}}{\fldrslt {\rtlch\fcs1 \af0 \ltrch\fcs0 \cs45\ul\cf22\insrsid15275006\charrsid8979329 \hich\af31506\dbch\af31505\loch\f31506
+https://fontawesome.com/icons/angle-down?s=solid}}}\sectd \ltrsect\linex0\endnhere\sectlinegrid360\sectdefaultcl\sftnbj {\rtlch\fcs1 \af0 \ltrch\fcs0 \insrsid15275006 \tab \tab \tab }{\rtlch\fcs1 \af0 \ltrch\fcs0
+\lang1024\langfe1024\noproof\insrsid11485576\charrsid7695206 {\*\shppict{\pict{\*\picprop\shplid1028{\sp{\sn shapeType}{\sv 75}}{\sp{\sn fFlipH}{\sv 0}}{\sp{\sn fFlipV}{\sv 0}}{\sp{\sn fLockRotation}{\sv 0}}{\sp{\sn fLockAspectRatio}{\sv 1}}
+{\sp{\sn fLockPosition}{\sv 0}}{\sp{\sn fLockAgainstSelect}{\sv 0}}{\sp{\sn fLockCropping}{\sv 0}}{\sp{\sn fLockVerticies}{\sv 0}}{\sp{\sn fLockAgainstGrouping}{\sv 0}}{\sp{\sn pictureGray}{\sv 0}}{\sp{\sn pictureBiLevel}{\sv 0}}{\sp{\sn fFilled}{\sv 0}}
+{\sp{\sn fNoFillHitTest}{\sv 0}}{\sp{\sn fLine}{\sv 0}}{\sp{\sn wzName}{\sv Picture 4}}{\sp{\sn dhgt}{\sv 251658240}}{\sp{\sn fHidden}{\sv 0}}{\sp{\sn fLayoutInCell}{\sv 1}}}\picscalex100\picscaley100\piccropl0\piccropr0\piccropt0\piccropb0
+\picw423\pich423\picwgoal240\pichgoal240\pngblip\bliptag1835713834{\*\blipuid 6d6ac52a4ba8f591c1feb5706fac1548}89504e470d0a1a0a0000000d49484452000000100000001008060000001ff3ff610000006049444154388d63181ee03c0303c37f060686f924f8663e540f482f
+98f19f0443e6a3e9c110c067084eb50d681220a70920691440f22a0c37a09b9e80c3106c9a1330dc86c710a235e3328424cdf80c215a33b221efa19864cd23063030300000a0a23c254c8dfedf0000000049454e44ae426082}}{\nonshppict
+{\pict\picscalex126\picscaley126\piccropl0\piccropr0\piccropt0\piccropb0\picw339\pich339\picwgoal192\pichgoal192\wmetafile8\bliptag1835713834{\*\blipuid 6d6ac52a4ba8f591c1feb5706fac1548}
+010009000003c300000000004500000000000400000003010800050000000b0200000000050000000c0211001100030000001e00040000000701040004000000
+070104000800000026060f000600544e5050060145000000410b8600ee0010001000000000001000100000000000280000001000000010000000010001000000
+0000000000000000000000000000000000000000000000000000ffffff000000b3220000000000000000000000000100494c03e0ffff07e00e320ff800001c78
+62c53c3e0000701f62c5e00f0000400701000000000000000000000000b345000000410bc6008800100010000000000010001000000000002800000010000000
+100000000100010000000000000000000000000000000000000000000000000000000000ffffff00ffff0101ffff0101ffff0101ffff0101feff0101fc1f0101
+f81f0101f0070101e3870101c3c101018fe001011ff00101bff80101ffff0101ffff0101ffff01010800000026060f000600544e50500701040000002701ffff030000000000}}}{\rtlch\fcs1 \af0 \ltrch\fcs0 \insrsid15275006
+\par }\pard \ltrpar\ql \li0\ri0\widctlpar\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0\pararsid9972925 {\rtlch\fcs1 \ab\af0 \ltrch\fcs0 \cs30\b\insrsid15275006\charrsid15275006 \hich\af31506\dbch\af31505\loch\f31506 Angle Up}{\rtlch\fcs1
+\af0 \ltrch\fcs0 \insrsid15275006 \hich\af31506\dbch\af31505\loch\f31506 : }{\field{\*\fldinst {\rtlch\fcs1 \af0 \ltrch\fcs0 \insrsid15275006 \hich\af31506\dbch\af31505\loch\f31506 HYPERLINK "}{\rtlch\fcs1 \af0 \ltrch\fcs0
+\insrsid15275006\charrsid15275006 \hich\af31506\dbch\af31505\loch\f31506 https://fontawesome.com/icons/angle-}{\rtlch\fcs1 \af0 \ltrch\fcs0 \insrsid15275006 \hich\af31506\dbch\af31505\loch\f31506 up}{\rtlch\fcs1 \af0 \ltrch\fcs0
+\insrsid15275006\charrsid15275006 \hich\af31506\dbch\af31505\loch\f31506 ?s=solid}{\rtlch\fcs1 \af0 \ltrch\fcs0 \insrsid15275006 \hich\af31506\dbch\af31505\loch\f31506 " }{\rtlch\fcs1 \af0 \ltrch\fcs0 \insrsid7748460 {\*\datafield
+00d0c9ea79f9bace118c8200aa004ba90b0200000003000000e0c9ea79f9bace118c8200aa004ba90b76000000680074007400700073003a002f002f0066006f006e00740061007700650073006f006d0065002e0063006f006d002f00690063006f006e0073002f0061006e0067006c0065002d00750070003f0073003d00
+73006f006c00690064000000795881f43b1d7f48af2c825dc485276300000000a5ab00030031}}}{\fldrslt {\rtlch\fcs1 \af0 \ltrch\fcs0 \cs45\ul\cf22\insrsid15275006\charrsid8979329 \hich\af31506\dbch\af31505\loch\f31506 https://fontawesome.com/icons/angle-up?s=solid}}}
+\sectd \ltrsect\linex0\endnhere\sectlinegrid360\sectdefaultcl\sftnbj {\rtlch\fcs1 \af0 \ltrch\fcs0 \insrsid15275006 \tab \tab \tab \tab }{\rtlch\fcs1 \af0 \ltrch\fcs0 \lang1024\langfe1024\noproof\insrsid11485576\charrsid7695206 {\*\shppict
+{\pict{\*\picprop\shplid1027{\sp{\sn shapeType}{\sv 75}}{\sp{\sn fFlipH}{\sv 0}}{\sp{\sn fFlipV}{\sv 0}}{\sp{\sn fLockRotation}{\sv 0}}{\sp{\sn fLockAspectRatio}{\sv 1}}{\sp{\sn fLockPosition}{\sv 0}}{\sp{\sn fLockAgainstSelect}{\sv 0}}
+{\sp{\sn fLockCropping}{\sv 0}}{\sp{\sn fLockVerticies}{\sv 0}}{\sp{\sn fLockAgainstGrouping}{\sv 0}}{\sp{\sn pictureGray}{\sv 0}}{\sp{\sn pictureBiLevel}{\sv 0}}{\sp{\sn fFilled}{\sv 0}}
+{\sp{\sn fNoFillHitTest}{\sv 0}}{\sp{\sn fLine}{\sv 0}}{\sp{\sn wzName}{\sv Picture 5}}{\sp{\sn dhgt}{\sv 251658240}}{\sp{\sn fHidden}{\sv 0}}{\sp{\sn fLayoutInCell}{\sv 1}}}\picscalex100\picscaley100\piccropl0\piccropr0\piccropt0\piccropb0
+\picw423\pich423\picwgoal240\pichgoal240\pngblip\bliptag1383733318{\*\blipuid 527a1846071df15fc9e51716336c8e62}89504e470d0a1a0a0000000d49484452000000100000001008060000001ff3ff610000005649444154388d6318de20818181e13f142790ea5364cd241b824d33
+d186a06b3e0fc54419824db300141334a401876618c06648034c723e9ac47c0cb7210056b518020400ba2170a711a319ddd520bd231b3030300000bef43c4518e0e4b30000000049454e44ae426082}}{\nonshppict{\pict\picscalex126\picscaley126\piccropl0\piccropr0\piccropt0\piccropb0
+\picw339\pich339\picwgoal192\pichgoal192\wmetafile8\bliptag1383733318{\*\blipuid 527a1846071df15fc9e51716336c8e62}010009000003c300000000004500000000000400000003010800050000000b0200000000050000000c0211001100030000001e00040000000701040004000000
+070104000800000026060f000600544e5050060145000000410b8600ee0010001000000000001000100000000000280000001000000010000000010001000000
+0000000000000000000000000000000000000000000000000000ffffff000000b3220000000000000000000000004007494cf00fffff701f0e323c3e00001c78
+62c50ff8000007e062c503e000000180010000000000000000000000000045000000410bc6008800100010000000000010001000000000002800000010000000
+100000000100010000000000000000000000000000000000000000000000000000000000ffffff00ffff0101ffff0101ffff0101ffff0101bff801010ff00101
+8fe00101c3c10101e3870101f0070101f81f0101fc1f0101fe7f0101ffff0101ffff0101ffff01010800000026060f000600544e50500701040000002701ffff030000000000}}}{\rtlch\fcs1 \af0 \ltrch\fcs0 \insrsid8019431
+\par }\pard \ltrpar\ql \li0\ri0\widctlpar\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0\pararsid6830075 {\rtlch\fcs1 \ab\af0 \ltrch\fcs0 \cs30\b\insrsid15275006\charrsid15275006 \hich\af31506\dbch\af31505\loch\f31506 Circle Question Regular}{
+\rtlch\fcs1 \af0 \ltrch\fcs0 \insrsid15275006 \hich\af31506\dbch\af31505\loch\f31506 : }{\field{\*\fldinst {\rtlch\fcs1 \af0 \ltrch\fcs0 \insrsid15275006 \hich\af31506\dbch\af31505\loch\f31506 HYPERLINK "}{\rtlch\fcs1 \af0 \ltrch\fcs0
+\insrsid15275006\charrsid15275006 \hich\af31506\dbch\af31505\loch\f31506 https://fontawesome.com/icons/circle-question?s=regular}{\rtlch\fcs1 \af0 \ltrch\fcs0 \insrsid15275006 \hich\af31506\dbch\af31505\loch\f31506 " }{\rtlch\fcs1 \af0 \ltrch\fcs0
+\insrsid7748460 {\*\datafield
+00d0c9ea79f9bace118c8200aa004ba90b0200000003000000e0c9ea79f9bace118c8200aa004ba90b88000000680074007400700073003a002f002f0066006f006e00740061007700650073006f006d0065002e0063006f006d002f00690063006f006e0073002f0063006900720063006c0065002d007100750065007300
+740069006f006e003f0073003d0072006500670075006c00610072000000795881f43b1d7f48af2c825dc485276300000000a5ab00030030}}}{\fldrslt {\rtlch\fcs1 \af0 \ltrch\fcs0 \cs45\ul\cf22\insrsid15275006\charrsid8979329 \hich\af31506\dbch\af31505\loch\f31506
+https://fontawesome.com/icons/circle-question?s=regular}}}\sectd \ltrsect\linex0\endnhere\sectlinegrid360\sectdefaultcl\sftnbj {\rtlch\fcs1 \af0 \ltrch\fcs0 \insrsid15275006 \tab }{\rtlch\fcs1 \af0 \ltrch\fcs0
+\lang1024\langfe1024\noproof\insrsid11485576\charrsid7695206 {\*\shppict{\pict{\*\picprop\shplid1026{\sp{\sn shapeType}{\sv 75}}{\sp{\sn fFlipH}{\sv 0}}{\sp{\sn fFlipV}{\sv 0}}{\sp{\sn fLockRotation}{\sv 0}}{\sp{\sn fLockAspectRatio}{\sv 1}}
+{\sp{\sn fLockPosition}{\sv 0}}{\sp{\sn fLockAgainstSelect}{\sv 0}}{\sp{\sn fLockCropping}{\sv 0}}{\sp{\sn fLockVerticies}{\sv 0}}{\sp{\sn fLockAgainstGrouping}{\sv 0}}{\sp{\sn pictureGray}{\sv 0}}{\sp{\sn pictureBiLevel}{\sv 0}}{\sp{\sn fFilled}{\sv 0}}
+{\sp{\sn fNoFillHitTest}{\sv 0}}{\sp{\sn fLine}{\sv 0}}{\sp{\sn wzName}{\sv Picture 7}}{\sp{\sn dhgt}{\sv 251658240}}{\sp{\sn fHidden}{\sv 0}}{\sp{\sn fLayoutInCell}{\sv 1}}}\picscalex100\picscaley100\piccropl0\piccropr0\piccropt0\piccropb0
+\picw423\pich423\picwgoal240\pichgoal240\pngblip\bliptag-472370675{\*\blipuid e3d8320db67b9cc56206ea40f2a7ce5f}89504e470d0a1a0a0000000d49484452000000100000001008060000001ff3ff61000000d449444154388da553810dc2300cb3260ed809e3839ec0293b012ee1
+849d503ed809f0414f185c30d4e24869d24a93b014ad6a13c7735a3430038800761391675d4c009eaa28af574632fb9325c91b1b1372416874093cdb995b9148
+e7c595792c4a49c1ac3aeb6e9bf140938b92e28918161a09d6838b6a20c696453242a351140d0114a9936f8d4bcab8d1aa1c5c498d3bdd7e00380378db8413bf
+a32bfde1c5b8b9138e71207be8ccfec368fd9a286b8e512023b4a8c698d1bb48578686bb48f23f7f5d6521d18f298feff063d238fe9c017c01290c636c26bcd8c10000000049454e44ae426082}}{\nonshppict{\pict\picscalex126\picscaley126\piccropl0\piccropr0\piccropt0\piccropb0
+\picw339\pich339\picwgoal192\pichgoal192\wmetafile8\bliptag-472370675{\*\blipuid e3d8320db67b9cc56206ea40f2a7ce5f}010009000003c300000000004500000000000400000003010800050000000b0200000000050000000c0211001100030000001e00040000000701040004000000
+070104000800000026060f000600544e5050060145000000410b8600ee0010001000000000001000100000000000280000001000000010000000010001000000
+0000000000000000000000000000000000000000000000000000ffffff0007d0b3223ff80000101c0000608e00004187494ce083ffffc1010e32c1c30000c061
+62c5c2230000476362c5e3e6000070040100383e00001d7800000fe0000045000000410bc6008800100010000000000010001000000000002800000010000000
+100000000100010000000000000000000000000000000000000000000000000000000000ffffff00f82f0101c0070000efe301009f710101be7800001f7c0000
+3efe00003e3c01013f9e01003ddc0101b89c01001c1901018ffb0001c7c10101e2870101f01f00010800000026060f000600544e50500701040000002701ffff030000000000}}}{\rtlch\fcs1 \af0 \ltrch\fcs0 \insrsid15275006
+\par }\pard \ltrpar\ql \li0\ri0\widctlpar\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0\pararsid14107051 {\rtlch\fcs1 \ab\af0 \ltrch\fcs0 \cs30\b\insrsid14107051\charrsid15795588 \hich\af31506\dbch\af31505\loch\f31506 Hands}{\rtlch\fcs1
+\ab\af0 \ltrch\fcs0 \cs30\b\insrsid15795588\charrsid15795588 \hich\af31506\dbch\af31505\loch\f31506 Cl}{\rtlch\fcs1 \ab\af0 \ltrch\fcs0 \cs30\b\insrsid14107051\charrsid15795588 \hich\af31506\dbch\af31505\loch\f31506 apping}{\rtlch\fcs1 \af0 \ltrch\fcs0
+\insrsid15795588 \hich\af31506\dbch\af31505\loch\f31506 : }{\field{\*\fldinst {\rtlch\fcs1 \af0 \ltrch\fcs0 \insrsid15795588 \hich\af31506\dbch\af31505\loch\f31506 HYPERLINK "}{\rtlch\fcs1 \af0 \ltrch\fcs0 \insrsid15795588\charrsid15795588
+\hich\af31506\dbch\af31505\loch\f31506 https://fontawesome.com/icons/hands-clapping?s=solid}{\rtlch\fcs1 \af0 \ltrch\fcs0 \insrsid15795588 \hich\af31506\dbch\af31505\loch\f31506 " }{\rtlch\fcs1 \af0 \ltrch\fcs0 \insrsid5193884 {\*\datafield
+00d0c9ea79f9bace118c8200aa004ba90b0200000003000000e0c9ea79f9bace118c8200aa004ba90b82000000680074007400700073003a002f002f0066006f006e00740061007700650073006f006d0065002e0063006f006d002f00690063006f006e0073002f00680061006e00640073002d0063006c00610070007000
+69006e0067003f0073003d0073006f006c00690064000000795881f43b1d7f48af2c825dc485276300000000a5ab000300}}}{\fldrslt {\rtlch\fcs1 \af0 \ltrch\fcs0 \cs45\ul\cf22\insrsid15795588\charrsid14120838 \hich\af31506\dbch\af31505\loch\f31506
+https://fontawesome.com/icons/hands-clapping?s=solid}}}\sectd \ltrsect\linex0\endnhere\sectlinegrid360\sectdefaultcl\sftnbj {\rtlch\fcs1 \af0 \ltrch\fcs0 \insrsid15795588 \tab \tab \tab }{\rtlch\fcs1 \af0 \ltrch\fcs0
+\lang1024\langfe1024\noproof\insrsid11485576\charrsid7695206 {\*\shppict{\pict{\*\picprop\shplid1025{\sp{\sn shapeType}{\sv 75}}{\sp{\sn fFlipH}{\sv 0}}{\sp{\sn fFlipV}{\sv 0}}{\sp{\sn fLockRotation}{\sv 0}}{\sp{\sn fLockAspectRatio}{\sv 1}}
+{\sp{\sn fLockPosition}{\sv 0}}{\sp{\sn fLockAgainstSelect}{\sv 0}}{\sp{\sn fLockCropping}{\sv 0}}{\sp{\sn fLockVerticies}{\sv 0}}{\sp{\sn fLockAgainstGrouping}{\sv 0}}{\sp{\sn pictureGray}{\sv 0}}{\sp{\sn pictureBiLevel}{\sv 0}}{\sp{\sn fFilled}{\sv 0}}
+{\sp{\sn fNoFillHitTest}{\sv 0}}{\sp{\sn fLine}{\sv 0}}{\sp{\sn wzName}{\sv Picture 12}}{\sp{\sn dhgt}{\sv 251658240}}{\sp{\sn fHidden}{\sv 0}}{\sp{\sn fLayoutInCell}{\sv 1}}}\picscalex100\picscaley100\piccropl0\piccropr0\piccropt0\piccropb0
+\picw423\pich423\picwgoal240\pichgoal240\pngblip\bliptag1295887634{\*\blipuid 4d3dad12b23a5735b114363a1b616845}89504e470d0a1a0a0000000d49484452000000100000001008060000001ff3ff61000000e349444154388d8d92cb11c2300c441fd0408edce0c831e92025a484
+d00125a4034a700994001d00378ee920741046193923fc09eccc8e2dd9bb923f1bf2688003f0021c500137cd6f815e94ebac1c4ae0a2e33e919bb08a64dfb803
+0fa0009ec04ec555b4d3c0699b1e45c02464a1d37154b6a98d39342a92964f0913311e94515ce8b9dc82496d62c2b8d3895b3071665e06f19418122639daeae3
+5a0d8ec0db9cb9cfdc5512a376d09a4e9658076b73cb22be2e083d23036bf28b52c43fb98f67fc63125ef239bc13ffa429cad3daea1227bfb69c51166d9b5255f2fef7495c007c005516860632aced3b0000000049454e44ae426082}}{\nonshppict
+{\pict\picscalex126\picscaley126\piccropl0\piccropr0\piccropt0\piccropb0\picw339\pich339\picwgoal192\pichgoal192\wmetafile8\bliptag1295887634{\*\blipuid 4d3dad12b23a5735b114363a1b616845}
+010009000003c300000000004500000000000400000003010800050000000b0200000000050000000c0211001100030000001e00040000000701040004000000
+070104000800000026060f000600544e5050060145000000410b8600ee0010001000000000001000100000000000280000001000000010000000010001000000
+0000000000000000000000000000000000000000000000000000ffffff000754b3220fee00001ff700007fff803f7ff7494cffffffff77f70e32efff00005df7
+62c53bb30000763362c52e0000000000010001b80000014c00000268ffff45000000410bc6008800100010000000000010001000000000002800000010000000
+100000000100010000000000000000000000000000000000000000000000000000000000ffffff00f8ab0101f0110000e0080100800001018008010100000000
+8808000110000001a2080100c44c000089cc0000d1ff0000ffff0000fe470000feb30000fd9700000800000026060f000600544e50500701040000002701ffff030000000000}}}{\rtlch\fcs1 \af0 \ltrch\fcs0 \insrsid14107051 \tab }{\rtlch\fcs1 \af0 \ltrch\fcs0
+\insrsid14107051\charrsid14107051
+\par }\pard \ltrpar\ql \li0\ri0\widctlpar\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0\pararsid6830075 {\rtlch\fcs1 \af0 \ltrch\fcs0 \insrsid15275006
+\par }\pard\plain \ltrpar\s1\ql \li0\ri0\sb240\sa60\keepn\widctlpar\wrapdefault\aspalpha\aspnum\faauto\outlinelevel0\adjustright\rin0\lin0\itap0\pararsid15275006 \rtlch\fcs1 \ab\af0\afs32\alang1025 \ltrch\fcs0
+\b\fs32\lang1033\langfe1033\kerning32\loch\af31502\hich\af31502\dbch\af31501\cgrid\langnp1033\langfenp1033 {\rtlch\fcs1 \af0 \ltrch\fcs0 \insrsid15275006 \hich\af31502\dbch\af31501\loch\f31502 Code Contributors}{\rtlch\fcs1 \af0 \ltrch\fcs0
+\insrsid15275006\charrsid15275006
+\par }\pard\plain \ltrpar\ql \li0\ri0\widctlpar\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0\pararsid6830075 \rtlch\fcs1 \af0\afs24\alang1025 \ltrch\fcs0
+\fs24\lang1033\langfe1033\loch\af31506\hich\af31506\dbch\af31505\cgrid\langnp1033\langfenp1033 {\rtlch\fcs1 \ab\af0 \ltrch\fcs0 \cs30\b\insrsid6830075\charrsid6830075 \hich\af31506\dbch\af31505\loch\f31506 Main Programmer}{\rtlch\fcs1 \af0 \ltrch\fcs0
+\insrsid6830075 \hich\af31506\dbch\af31505\loch\f31506 : }{\rtlch\fcs1 \af0 \ltrch\fcs0 \insrsid4869634 \hich\af31506\dbch\af31505\loch\f31506 spel werdz rit}{\rtlch\fcs1 \af0 \ltrch\fcs0 \insrsid5639150 \hich\af31506\dbch\af31505\loch\f31506 e}{
+\rtlch\fcs1 \af0 \ltrch\fcs0 \insrsid6830075 \hich\af31506\dbch\af31505\loch\f31506 (}{\field{\*\fldinst {\rtlch\fcs1 \af0 \ltrch\fcs0 \insrsid6830075 \hich\af31506\dbch\af31505\loch\f31506 HYPERLINK "}{\rtlch\fcs1 \af0 \ltrch\fcs0
+\insrsid6830075\charrsid6830075 \hich\af31506\dbch\af31505\loch\f31506 https://github.com/bonimy}{\rtlch\fcs1 \af0 \ltrch\fcs0 \insrsid6830075 \hich\af31506\dbch\af31505\loch\f31506 " }{\rtlch\fcs1 \af0 \ltrch\fcs0 \insrsid13263836 {\*\datafield
+00d0c9ea79f9bace118c8200aa004ba90b0200000003000000e0c9ea79f9bace118c8200aa004ba90b4c000000680074007400700073003a002f002f006700690074006800750062002e0063006f006d002f0062006f006e0069006d0079000000795881f43b1d7f48af2c825dc485276300000000a5ab0003002f0000}}
+}{\fldrslt {\rtlch\fcs1 \af0 \ltrch\fcs0 \cs45\ul\cf22\insrsid6830075\charrsid1798261 \hich\af31506\dbch\af31505\loch\f31506 https://github.com/bonimy}}}\sectd \ltrsect\linex0\endnhere\sectlinegrid360\sectdefaultcl\sftnbj {\rtlch\fcs1 \af0 \ltrch\fcs0
+\insrsid6830075 \hich\af31506\dbch\af31505\loch\f31506 )
+\par }{\rtlch\fcs1 \af0 \ltrch\fcs0 \insrsid15275006
+\par }\pard\plain \ltrpar\s1\ql \li0\ri0\sb240\sa60\keepn\widctlpar\wrapdefault\aspalpha\aspnum\faauto\outlinelevel0\adjustright\rin0\lin0\itap0\pararsid15275006 \rtlch\fcs1 \ab\af0\afs32\alang1025 \ltrch\fcs0
+\b\fs32\lang1033\langfe1033\kerning32\loch\af31502\hich\af31502\dbch\af31501\cgrid\langnp1033\langfenp1033 {\rtlch\fcs1 \af0 \ltrch\fcs0 \insrsid15275006 \hich\af31502\dbch\af31501\loch\f31502 Special Thanks
+\par }\pard\plain \ltrpar\s2\ql \li0\ri0\sb240\sa60\keepn\widctlpar\wrapdefault\aspalpha\aspnum\faauto\outlinelevel1\adjustright\rin0\lin0\itap0\pararsid15888887 \rtlch\fcs1 \ab\ai\af0\afs28\alang1025 \ltrch\fcs0
+\b\i\fs28\lang1033\langfe1033\loch\af31502\hich\af31502\dbch\af31501\cgrid\langnp1033\langfenp1033 {\rtlch\fcs1 \af0 \ltrch\fcs0 \insrsid15888887 \hich\af31502\dbch\af31501\loch\f31502 The SMAS Disassembly Team}{\rtlch\fcs1 \af0 \ltrch\fcs0
+\insrsid15888887\charrsid15888887
+\par }\pard\plain \ltrpar\ql \li0\ri0\widctlpar\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0\pararsid15275006 \rtlch\fcs1 \af0\afs24\alang1025 \ltrch\fcs0
+\fs24\lang1033\langfe1033\loch\af31506\hich\af31506\dbch\af31505\cgrid\langnp1033\langfenp1033 {\rtlch\fcs1 \af0 \ltrch\fcs0 \lang2058\langfe1033\langnp2058\insrsid15275006\charrsid4869634 \hich\af31506\dbch\af31505\loch\f31506 Ersanio (}
+{\field{\*\fldinst {\rtlch\fcs1 \af0 \ltrch\fcs0 \lang2058\langfe1033\langnp2058\insrsid15275006\charrsid4869634 \hich\af31506\dbch\af31505\loch\f31506 HYPERLINK "\hich\af31506\dbch\af31505\loch\f31506 https://twitter.com/Ersanio
+\hich\af31506\dbch\af31505\loch\f31506 " }{\rtlch\fcs1 \af0 \ltrch\fcs0 \insrsid7748460 {\*\datafield
+00d0c9ea79f9bace118c8200aa004ba90b0200000003000000e0c9ea79f9bace118c8200aa004ba90b50000000680074007400700073003a002f002f0074007700690074007400650072002e0063006f006d002f0045007200730061006e0069006f000000795881f43b1d7f48af2c825dc485276300000000a5ab00030000}
+}}{\fldrslt {\rtlch\fcs1 \af0 \ltrch\fcs0 \cs45\ul\cf22\lang2058\langfe1033\langnp2058\insrsid15275006\charrsid4869634 \hich\af31506\dbch\af31505\loch\f31506 https://twitter.com/Ersanio}}}\sectd \ltrsect
+\linex0\endnhere\sectlinegrid360\sectdefaultcl\sftnbj {\rtlch\fcs1 \af0 \ltrch\fcs0 \lang2058\langfe1033\langnp2058\insrsid15275006\charrsid4869634 \hich\af31506\dbch\af31505\loch\f31506 )
+\par }{\rtlch\fcs1 \af0 \ltrch\fcs0 \lang2058\langfe1033\langnp2058\insrsid15888887\charrsid4869634 \hich\af31506\dbch\af31505\loch\f31506 i}{\rtlch\fcs1 \af0 \ltrch\fcs0 \lang2058\langfe1033\langnp2058\insrsid15275006\charrsid4869634
+\hich\af31506\dbch\af31505\loch\f31506 mamelia (}{\field{\*\fldinst {\rtlch\fcs1 \af0 \ltrch\fcs0 \lang2058\langfe1033\langnp2058\insrsid15275006\charrsid4869634 \hich\af31506\dbch\af31505\loch\f31506 HYPERLINK "\hich\af31506\dbch\af31505\loch\f31506
+https://www.smwcentral.net/?p=profile&id=3471\hich\af31506\dbch\af31505\loch\f31506 " }{\rtlch\fcs1 \af0 \ltrch\fcs0 \insrsid7748460 {\*\datafield
+00d0c9ea79f9bace118c8200aa004ba90b0200000003000000e0c9ea79f9bace118c8200aa004ba90b74000000680074007400700073003a002f002f007700770077002e0073006d007700630065006e007400720061006c002e006e00650074002f003f0070003d00700072006f00660069006c0065002600690064003d00
+33003400370031000000795881f43b1d7f48af2c825dc485276300000000a5ab00030000}}}{\fldrslt {\rtlch\fcs1 \af0 \ltrch\fcs0 \cs45\ul\cf22\lang2058\langfe1033\langnp2058\insrsid15275006\charrsid4869634 \hich\af31506\dbch\af31505\loch\f31506
+https://www.smwcentral.net/?p=profile&id=3471}}}\sectd \ltrsect\linex0\endnhere\sectlinegrid360\sectdefaultcl\sftnbj {\rtlch\fcs1 \af0 \ltrch\fcs0 \lang2058\langfe1033\langnp2058\insrsid15275006\charrsid4869634 \hich\af31506\dbch\af31505\loch\f31506 )
+
+\par }{\rtlch\fcs1 \af0 \ltrch\fcs0 \lang2058\langfe1033\langnp2058\insrsid15888887\charrsid4869634 \hich\af31506\dbch\af31505\loch\f31506 Roy}{\rtlch\fcs1 \af0 \ltrch\fcs0 \lang2058\langfe1033\langnp2058\insrsid7748460\charrsid4869634
+\hich\af31506\dbch\af31505\loch\f31506 /FuzzyFreak}{\rtlch\fcs1 \af0 \ltrch\fcs0 \lang2058\langfe1033\langnp2058\insrsid15888887\charrsid4869634
+\par }{\rtlch\fcs1 \af0 \ltrch\fcs0 \lang2058\langfe1033\langnp2058\insrsid7748460\charrsid4869634 \hich\af31506\dbch\af31505\loch\f31506 Alcaro\hich\af31506\dbch\af31505\loch\f31506 }{\field{\*\fldinst {\rtlch\fcs1 \af0 \ltrch\fcs0
+\lang2058\langfe1033\langnp2058\insrsid7748460\charrsid4869634 \hich\af31506\dbch\af31505\loch\f31506 HYPERLINK "\hich\af31506\dbch\af31505\loch\f31506 https://github.com/Alcaro/\hich\af31506\dbch\af31505\loch\f31506 " }{\rtlch\fcs1 \af0 \ltrch\fcs0
+\insrsid5193884 {\*\datafield
+00d0c9ea79f9bace118c8200aa004ba90b0200000003000000e0c9ea79f9bace118c8200aa004ba90b4e000000680074007400700073003a002f002f006700690074006800750062002e0063006f006d002f0041006c006300610072006f002f000000795881f43b1d7f48af2c825dc485276300000000a5ab000362}}
+}{\fldrslt {\rtlch\fcs1 \af0 \ltrch\fcs0 \cs45\ul\cf22\lang2058\langfe1033\langnp2058\insrsid7748460\charrsid4869634 \hich\af31506\dbch\af31505\loch\f31506 https://github.com/Alcaro/}}}\sectd \ltrsect\linex0\endnhere\sectlinegrid360\sectdefaultcl\sftnbj {
+\rtlch\fcs1 \af0 \ltrch\fcs0 \lang2058\langfe1033\langnp2058\insrsid7748460\charrsid4869634
+\par }{\rtlch\fcs1 \af0 \ltrch\fcs0 \insrsid7748460 \hich\af31506\dbch\af31505\loch\f31506 BlueRabbit
+\par \hich\af31506\dbch\af31505\loch\f31506 MiOR
+\par \hich\af31506\dbch\af31505\loch\f31506 Dosarecool
+\par \hich\af31506\dbch\af31505\loch\f31506 wiiqwertyuip
+\par }{\rtlch\fcs1 \af0 \ltrch\fcs0 \insrsid15888887
+\par }{\rtlch\fcs1 \af0 \ltrch\fcs0 \insrsid15275006
+\par }\pard\plain \ltrpar\s2\ql \li0\ri0\sb240\sa60\keepn\widctlpar\wrapdefault\aspalpha\aspnum\faauto\outlinelevel1\adjustright\rin0\lin0\itap0\pararsid15888887 \rtlch\fcs1 \ab\ai\af0\afs28\alang1025 \ltrch\fcs0
+\b\i\fs28\lang1033\langfe1033\loch\af31502\hich\af31502\dbch\af31501\cgrid\langnp1033\langfenp1033 {\rtlch\fcs1 \af0 \ltrch\fcs0 \insrsid15888887 \hich\af31502\dbch\af31501\loch\f31502 Testers
+\par }\pard\plain \ltrpar\ql \li0\ri0\widctlpar\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0\pararsid15275006 \rtlch\fcs1 \af0\afs24\alang1025 \ltrch\fcs0
+\fs24\lang1033\langfe1033\loch\af31506\hich\af31506\dbch\af31505\cgrid\langnp1033\langfenp1033 {\rtlch\fcs1 \af0 \ltrch\fcs0 \insrsid15275006 \hich\af31506\dbch\af31505\loch\f31506 Yoshifanatic1}{\rtlch\fcs1 \af0 \ltrch\fcs0 \insrsid7748460
+\hich\af31506\dbch\af31505\loch\f31506 (}{\field{\*\fldinst {\rtlch\fcs1 \af0 \ltrch\fcs0 \insrsid7748460 \hich\af31506\dbch\af31505\loch\f31506 HYPERLINK "}{\rtlch\fcs1 \af0 \ltrch\fcs0 \insrsid7748460\charrsid7748460
+\hich\af31506\dbch\af31505\loch\f31506 https://www.youtube.com/user/Yoshifanatic1}{\rtlch\fcs1 \af0 \ltrch\fcs0 \insrsid7748460 \hich\af31506\dbch\af31505\loch\f31506 " }{\rtlch\fcs1 \af0 \ltrch\fcs0 \insrsid5193884 {\*\datafield
+00d0c9ea79f9bace118c8200aa004ba90b0200000003000000e0c9ea79f9bace118c8200aa004ba90b6e000000680074007400700073003a002f002f007700770077002e0079006f00750074007500620065002e0063006f006d002f0075007300650072002f0059006f00730068006900660061006e006100740069006300
+31000000795881f43b1d7f48af2c825dc485276300000000a5ab000300}}}{\fldrslt {\rtlch\fcs1 \af0 \ltrch\fcs0 \cs45\ul\cf22\insrsid7748460\charrsid14120838 \hich\af31506\dbch\af31505\loch\f31506 https://www.youtube.com/user/Yoshifanatic1}}}\sectd \ltrsect
+\linex0\endnhere\sectlinegrid360\sectdefaultcl\sftnbj {\rtlch\fcs1 \af0 \ltrch\fcs0 \insrsid7748460 \hich\af31506\dbch\af31505\loch\f31506 )}{\rtlch\fcs1 \af0 \ltrch\fcs0 \insrsid15275006\charrsid15275006
+\par }\pard \ltrpar\ql \li0\ri0\widctlpar\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0\pararsid6830075 {\rtlch\fcs1 \af0 \ltrch\fcs0 \insrsid15888887 \hich\af31506\dbch\af31505\loch\f31506 Herega
+\par }{\rtlch\fcs1 \af0 \ltrch\fcs0 \insrsid6830075
+\par }{\rtlch\fcs1 \af0 \ltrch\fcs0 \insrsid8019431 \hich\af31506\dbch\af31505\loch\f31506 If I forgot you, please let me know.}{\rtlch\fcs1 \af0 \ltrch\fcs0 \insrsid8019431\charrsid6830075
+\par }{\*\themedata 504b030414000600080000002100e9de0fbfff0000001c020000130000005b436f6e74656e745f54797065735d2e786d6cac91cb4ec3301045f748fc83e52d4a
+9cb2400825e982c78ec7a27cc0c8992416c9d8b2a755fbf74cd25442a820166c2cd933f79e3be372bd1f07b5c3989ca74aaff2422b24eb1b475da5df374fd9ad
+5689811a183c61a50f98f4babebc2837878049899a52a57be670674cb23d8e90721f90a4d2fa3802cb35762680fd800ecd7551dc18eb899138e3c943d7e503b6
+b01d583deee5f99824e290b4ba3f364eac4a430883b3c092d4eca8f946c916422ecab927f52ea42b89a1cd59c254f919b0e85e6535d135a8de20f20b8c12c3b0
+0c895fcf6720192de6bf3b9e89ecdbd6596cbcdd8eb28e7c365ecc4ec1ff1460f53fe813d3cc7f5b7f020000ffff0300504b030414000600080000002100a5d6
+a7e7c0000000360100000b0000005f72656c732f2e72656c73848fcf6ac3300c87ef85bd83d17d51d2c31825762fa590432fa37d00e1287f68221bdb1bebdb4f
+c7060abb0884a4eff7a93dfeae8bf9e194e720169aaa06c3e2433fcb68e1763dbf7f82c985a4a725085b787086a37bdbb55fbc50d1a33ccd311ba548b6309512
+0f88d94fbc52ae4264d1c910d24a45db3462247fa791715fd71f989e19e0364cd3f51652d73760ae8fa8c9ffb3c330cc9e4fc17faf2ce545046e37944c69e462
+a1a82fe353bd90a865aad41ed0b5b8f9d6fd010000ffff0300504b0304140006000800000021006b799616830000008a0000001c0000007468656d652f746865
+6d652f7468656d654d616e616765722e786d6c0ccc4d0ac3201040e17da17790d93763bb284562b2cbaebbf600439c1a41c7a0d29fdbd7e5e38337cedf14d59b
+4b0d592c9c070d8a65cd2e88b7f07c2ca71ba8da481cc52c6ce1c715e6e97818c9b48d13df49c873517d23d59085adb5dd20d6b52bd521ef2cdd5eb9246a3d8b
+4757e8d3f729e245eb2b260a0238fd010000ffff0300504b03041400060008000000210030dd4329a8060000a41b0000160000007468656d652f7468656d652f
+7468656d65312e786d6cec594f6fdb3614bf0fd87720746f6327761a07758ad8b19b2d4d1bc46e871e698996d850a240d2497d1bdae38001c3ba618715d86d87
+615b8116d8a5fb34d93a6c1dd0afb0475292c5585e9236d88aad3e2412f9e3fbff1e1fa9abd7eec70c1d1221294fda5efd72cd4324f1794093b0eddd1ef62fad
+79482a9c0498f184b4bd2991deb58df7dfbb8ad755446282607d22d771db8b944ad79796a40fc3585ee62949606ecc458c15bc8a702910f808e8c66c69b9565b
+5d8a314d3c94e018c8de1a8fa94fd05093f43672e23d06af89927ac06762a049136785c10607758d9053d965021d62d6f6804fc08f86e4bef210c352c144dbab
+999fb7b4717509af678b985ab0b6b4ae6f7ed9ba6c4170b06c788a705430adf71bad2b5b057d03606a1ed7ebf5babd7a41cf00b0ef83a6569632cd467faddec9
+699640f6719e76b7d6ac355c7c89feca9cccad4ea7d36c65b258a206641f1b73f8b5da6a6373d9c11b90c537e7f08dce66b7bbeae00dc8e257e7f0fd2badd586
+8b37a088d1e4600ead1ddaef67d40bc898b3ed4af81ac0d76a197c86826828a24bb318f3442d8ab518dfe3a20f000d6458d104a9694ac6d88728eee2782428d6
+0cf03ac1a5193be4cbb921cd0b495fd054b5bd0f530c1931a3f7eaf9f7af9e3f45c70f9e1d3ff8e9f8e1c3e3073f5a42ceaa6d9c84e5552fbffdeccfc71fa33f
+9e7ef3f2d117d57859c6fffac327bffcfc793510d26726ce8b2f9ffcf6ecc98baf3efdfdbb4715f04d814765f890c644a29be408edf3181433567125272371be
+15c308d3f28acd249438c19a4b05fd9e8a1cf4cd296699771c393ac4b5e01d01e5a30a787d72cf1178108989a2159c77a2d801ee72ce3a5c545a6147f32a9979
+3849c26ae66252c6ed637c58c5bb8b13c7bfbd490a75330f4b47f16e441c31f7184e140e494214d273fc80900aedee52ead87597fa824b3e56e82e451d4c2b4d
+32a423279a668bb6690c7e9956e90cfe766cb37b077538abd27a8b1cba48c80acc2a841f12e698f13a9e281c57911ce298950d7e03aba84ac8c154f8655c4f2a
+f074481847bd804859b5e696007d4b4edfc150b12addbecba6b18b148a1e54d1bc81392f23b7f84137c2715a851dd0242a633f900710a218ed715505dfe56e86
+e877f0034e16bafb0e258ebb4faf06b769e888340b103d331115bebc4eb813bf83291b63624a0d1475a756c734f9bbc2cd28546ecbe1e20a3794ca175f3fae90
+fb6d2dd99bb07b55e5ccf68942bd0877b23c77b908e8db5f9db7f024d9239010f35bd4bbe2fcae387bfff9e2bc289f2fbe24cfaa301468dd8bd846dbb4ddf1c2
+ae7b4c191ba8292337a469bc25ec3d411f06f53a73e224c5292c8de0516732307070a1c0660d125c7d44553488700a4d7bddd3444299910e254ab984c3a219ae
+a4adf1d0f82b7bd46cea4388ad1c12ab5d1ed8e1153d9c9f350a3246aad01c6873462b9ac05999ad5cc988826eafc3acae853a33b7ba11cd1445875ba1b236b1
+399483c90bd560b0b0263435085a21b0f22a9cf9356b38ec6046026d77eba3dc2dc60b17e92219e180643ed27acffba86e9c94c7ca9c225a0f1b0cfae0788ad5
+4adc5a9aec1b703b8b93caec1a0bd8e5de7b132fe5113cf312503b998e2c2927274bd051db6b35979b1ef271daf6c6704e86c73805af4bdd476216c26593af84
+0dfb5393d964f9cc9bad5c313709ea70f561ed3ea7b053075221d51696910d0d339585004b34272bff7213cc7a510a5454a3b349b1b206c1f0af490176745d4b
+c663e2abb2b34b23da76f6352ba57ca2881844c1111ab189d8c7e07e1daaa04f40255c77988aa05fe06e4e5bdb4cb9c5394bbaf28d98c1d971ccd20867e556a7
+689ec9166e0a522183792b8907ba55ca6e943bbf2a26e52f48957218ffcf54d1fb09dc3eac04da033e5c0d0b8c74a6b43d2e54c4a10aa511f5fb021a07533b20
+5ae07e17a621a8e082dafc17e450ffb739676998b48643a4daa7211214f623150942f6a02c99e83b85583ddbbb2c4996113211551257a656ec1139246ca86be0
+aadedb3d1441a89b6a929501833b197fee7b9641a3503739e57c732a59b1f7da1cf8a73b1f9bcca0945b874d4393dbbf10b1680f66bbaa5d6f96e77b6f59113d
+316bb31a795600b3d256d0cad2fe354538e7566b2bd69cc6cbcd5c38f0e2bcc63058344429dc2121fd07f63f2a7c66bf76e80d75c8f7a1b622f878a18941d840
+545fb28d07d205d20e8ea071b283369834296bdaac75d256cb37eb0bee740bbe278cad253b8bbfcf69eca23973d939b97891c6ce2cecd8da8e2d343578f6648a
+c2d0383fc818c798cf64e52f597c740f1cbd05df0c264c49134cf09d4a60e8a107260f20f92d47b374e32f000000ffff0300504b030414000600080000002100
+0dd1909fb60000001b010000270000007468656d652f7468656d652f5f72656c732f7468656d654d616e616765722e786d6c2e72656c73848f4d0ac2301484f7
+8277086f6fd3ba109126dd88d0add40384e4350d363f2451eced0dae2c082e8761be9969bb979dc9136332de3168aa1a083ae995719ac16db8ec8e4052164e89
+d93b64b060828e6f37ed1567914b284d262452282e3198720e274a939cd08a54f980ae38a38f56e422a3a641c8bbd048f7757da0f19b017cc524bd62107bd500
+1996509affb3fd381a89672f1f165dfe514173d9850528a2c6cce0239baa4c04ca5bbabac4df000000ffff0300504b01022d0014000600080000002100e9de0f
+bfff0000001c0200001300000000000000000000000000000000005b436f6e74656e745f54797065735d2e786d6c504b01022d0014000600080000002100a5d6
+a7e7c0000000360100000b00000000000000000000000000300100005f72656c732f2e72656c73504b01022d00140006000800000021006b799616830000008a
+0000001c00000000000000000000000000190200007468656d652f7468656d652f7468656d654d616e616765722e786d6c504b01022d00140006000800000021
+0030dd4329a8060000a41b00001600000000000000000000000000d60200007468656d652f7468656d652f7468656d65312e786d6c504b01022d001400060008
+00000021000dd1909fb60000001b0100002700000000000000000000000000b20900007468656d652f7468656d652f5f72656c732f7468656d654d616e616765722e786d6c2e72656c73504b050600000000050005005d010000ad0a00000000}
+{\*\colorschememapping 3c3f786d6c2076657273696f6e3d22312e302220656e636f64696e673d225554462d3822207374616e64616c6f6e653d22796573223f3e0d0a3c613a636c724d
+617020786d6c6e733a613d22687474703a2f2f736368656d61732e6f70656e786d6c666f726d6174732e6f72672f64726177696e676d6c2f323030362f6d6169
+6e22206267313d226c743122207478313d22646b3122206267323d226c743222207478323d22646b322220616363656e74313d22616363656e74312220616363
+656e74323d22616363656e74322220616363656e74333d22616363656e74332220616363656e74343d22616363656e74342220616363656e74353d22616363656e74352220616363656e74363d22616363656e74362220686c696e6b3d22686c696e6b2220666f6c486c696e6b3d22666f6c486c696e6b222f3e}
+{\*\latentstyles\lsdstimax376\lsdlockeddef0\lsdsemihiddendef0\lsdunhideuseddef0\lsdqformatdef0\lsdprioritydef99{\lsdlockedexcept \lsdqformat1 \lsdpriority0 \lsdlocked0 Normal;\lsdqformat1 \lsdpriority9 \lsdlocked0 heading 1;
+\lsdsemihidden1 \lsdunhideused1 \lsdqformat1 \lsdpriority9 \lsdlocked0 heading 2;\lsdsemihidden1 \lsdunhideused1 \lsdqformat1 \lsdpriority9 \lsdlocked0 heading 3;\lsdsemihidden1 \lsdunhideused1 \lsdqformat1 \lsdpriority9 \lsdlocked0 heading 4;
+\lsdsemihidden1 \lsdunhideused1 \lsdqformat1 \lsdpriority9 \lsdlocked0 heading 5;\lsdsemihidden1 \lsdunhideused1 \lsdqformat1 \lsdpriority9 \lsdlocked0 heading 6;\lsdsemihidden1 \lsdunhideused1 \lsdqformat1 \lsdpriority9 \lsdlocked0 heading 7;
+\lsdsemihidden1 \lsdunhideused1 \lsdqformat1 \lsdpriority9 \lsdlocked0 heading 8;\lsdsemihidden1 \lsdunhideused1 \lsdqformat1 \lsdpriority9 \lsdlocked0 heading 9;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 index 1;
+\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 index 2;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 index 3;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 index 4;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 index 5;
+\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 index 6;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 index 7;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 index 8;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 index 9;
+\lsdsemihidden1 \lsdunhideused1 \lsdpriority39 \lsdlocked0 toc 1;\lsdsemihidden1 \lsdunhideused1 \lsdpriority39 \lsdlocked0 toc 2;\lsdsemihidden1 \lsdunhideused1 \lsdpriority39 \lsdlocked0 toc 3;
+\lsdsemihidden1 \lsdunhideused1 \lsdpriority39 \lsdlocked0 toc 4;\lsdsemihidden1 \lsdunhideused1 \lsdpriority39 \lsdlocked0 toc 5;\lsdsemihidden1 \lsdunhideused1 \lsdpriority39 \lsdlocked0 toc 6;
+\lsdsemihidden1 \lsdunhideused1 \lsdpriority39 \lsdlocked0 toc 7;\lsdsemihidden1 \lsdunhideused1 \lsdpriority39 \lsdlocked0 toc 8;\lsdsemihidden1 \lsdunhideused1 \lsdpriority39 \lsdlocked0 toc 9;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Normal Indent;
+\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 footnote text;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 annotation text;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 header;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 footer;
+\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 index heading;\lsdsemihidden1 \lsdunhideused1 \lsdpriority35 \lsdlocked0 caption;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 table of figures;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 envelope address;
+\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 envelope return;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 footnote reference;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 annotation reference;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 line number;
+\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 page number;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 endnote reference;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 endnote text;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 table of authorities;
+\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 macro;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 toa heading;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Bullet;
+\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Number;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List 2;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List 3;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List 4;
+\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List 5;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Bullet 2;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Bullet 3;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Bullet 4;
+\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Bullet 5;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Number 2;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Number 3;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Number 4;
+\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Number 5;\lsdqformat1 \lsdpriority10 \lsdlocked0 Title;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Closing;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Signature;
+\lsdsemihidden1 \lsdunhideused1 \lsdpriority1 \lsdlocked0 Default Paragraph Font;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Body Text;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Body Text Indent;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Continue;
+\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Continue 2;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Continue 3;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Continue 4;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Continue 5;
+\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Message Header;\lsdqformat1 \lsdpriority11 \lsdlocked0 Subtitle;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Salutation;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Date;
+\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Body Text First Indent;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Body Text First Indent 2;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Note Heading;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Body Text 2;
+\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Body Text 3;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Body Text Indent 2;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Body Text Indent 3;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Block Text;
+\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Hyperlink;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 FollowedHyperlink;\lsdqformat1 \lsdpriority22 \lsdlocked0 Strong;\lsdqformat1 \lsdpriority20 \lsdlocked0 Emphasis;
+\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Document Map;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Plain Text;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 E-mail Signature;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Top of Form;
+\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Bottom of Form;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Normal (Web);\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Acronym;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Address;
+\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Cite;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Code;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Definition;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Keyboard;
+\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Preformatted;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Sample;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Typewriter;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Variable;
+\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 annotation subject;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 No List;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Outline List 1;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Outline List 2;
+\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Outline List 3;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Balloon Text;\lsdpriority59 \lsdlocked0 Table Grid;\lsdsemihidden1 \lsdlocked0 Placeholder Text;\lsdqformat1 \lsdpriority1 \lsdlocked0 No Spacing;
+\lsdpriority60 \lsdlocked0 Light Shading;\lsdpriority61 \lsdlocked0 Light List;\lsdpriority62 \lsdlocked0 Light Grid;\lsdpriority63 \lsdlocked0 Medium Shading 1;\lsdpriority64 \lsdlocked0 Medium Shading 2;\lsdpriority65 \lsdlocked0 Medium List 1;
+\lsdpriority66 \lsdlocked0 Medium List 2;\lsdpriority67 \lsdlocked0 Medium Grid 1;\lsdpriority68 \lsdlocked0 Medium Grid 2;\lsdpriority69 \lsdlocked0 Medium Grid 3;\lsdpriority70 \lsdlocked0 Dark List;\lsdpriority71 \lsdlocked0 Colorful Shading;
+\lsdpriority72 \lsdlocked0 Colorful List;\lsdpriority73 \lsdlocked0 Colorful Grid;\lsdpriority60 \lsdlocked0 Light Shading Accent 1;\lsdpriority61 \lsdlocked0 Light List Accent 1;\lsdpriority62 \lsdlocked0 Light Grid Accent 1;
+\lsdpriority63 \lsdlocked0 Medium Shading 1 Accent 1;\lsdpriority64 \lsdlocked0 Medium Shading 2 Accent 1;\lsdpriority65 \lsdlocked0 Medium List 1 Accent 1;\lsdsemihidden1 \lsdlocked0 Revision;\lsdqformat1 \lsdpriority34 \lsdlocked0 List Paragraph;
+\lsdqformat1 \lsdpriority29 \lsdlocked0 Quote;\lsdqformat1 \lsdpriority30 \lsdlocked0 Intense Quote;\lsdpriority66 \lsdlocked0 Medium List 2 Accent 1;\lsdpriority67 \lsdlocked0 Medium Grid 1 Accent 1;\lsdpriority68 \lsdlocked0 Medium Grid 2 Accent 1;
+\lsdpriority69 \lsdlocked0 Medium Grid 3 Accent 1;\lsdpriority70 \lsdlocked0 Dark List Accent 1;\lsdpriority71 \lsdlocked0 Colorful Shading Accent 1;\lsdpriority72 \lsdlocked0 Colorful List Accent 1;\lsdpriority73 \lsdlocked0 Colorful Grid Accent 1;
+\lsdpriority60 \lsdlocked0 Light Shading Accent 2;\lsdpriority61 \lsdlocked0 Light List Accent 2;\lsdpriority62 \lsdlocked0 Light Grid Accent 2;\lsdpriority63 \lsdlocked0 Medium Shading 1 Accent 2;\lsdpriority64 \lsdlocked0 Medium Shading 2 Accent 2;
+\lsdpriority65 \lsdlocked0 Medium List 1 Accent 2;\lsdpriority66 \lsdlocked0 Medium List 2 Accent 2;\lsdpriority67 \lsdlocked0 Medium Grid 1 Accent 2;\lsdpriority68 \lsdlocked0 Medium Grid 2 Accent 2;\lsdpriority69 \lsdlocked0 Medium Grid 3 Accent 2;
+\lsdpriority70 \lsdlocked0 Dark List Accent 2;\lsdpriority71 \lsdlocked0 Colorful Shading Accent 2;\lsdpriority72 \lsdlocked0 Colorful List Accent 2;\lsdpriority73 \lsdlocked0 Colorful Grid Accent 2;\lsdpriority60 \lsdlocked0 Light Shading Accent 3;
+\lsdpriority61 \lsdlocked0 Light List Accent 3;\lsdpriority62 \lsdlocked0 Light Grid Accent 3;\lsdpriority63 \lsdlocked0 Medium Shading 1 Accent 3;\lsdpriority64 \lsdlocked0 Medium Shading 2 Accent 3;\lsdpriority65 \lsdlocked0 Medium List 1 Accent 3;
+\lsdpriority66 \lsdlocked0 Medium List 2 Accent 3;\lsdpriority67 \lsdlocked0 Medium Grid 1 Accent 3;\lsdpriority68 \lsdlocked0 Medium Grid 2 Accent 3;\lsdpriority69 \lsdlocked0 Medium Grid 3 Accent 3;\lsdpriority70 \lsdlocked0 Dark List Accent 3;
+\lsdpriority71 \lsdlocked0 Colorful Shading Accent 3;\lsdpriority72 \lsdlocked0 Colorful List Accent 3;\lsdpriority73 \lsdlocked0 Colorful Grid Accent 3;\lsdpriority60 \lsdlocked0 Light Shading Accent 4;\lsdpriority61 \lsdlocked0 Light List Accent 4;
+\lsdpriority62 \lsdlocked0 Light Grid Accent 4;\lsdpriority63 \lsdlocked0 Medium Shading 1 Accent 4;\lsdpriority64 \lsdlocked0 Medium Shading 2 Accent 4;\lsdpriority65 \lsdlocked0 Medium List 1 Accent 4;\lsdpriority66 \lsdlocked0 Medium List 2 Accent 4;
+\lsdpriority67 \lsdlocked0 Medium Grid 1 Accent 4;\lsdpriority68 \lsdlocked0 Medium Grid 2 Accent 4;\lsdpriority69 \lsdlocked0 Medium Grid 3 Accent 4;\lsdpriority70 \lsdlocked0 Dark List Accent 4;\lsdpriority71 \lsdlocked0 Colorful Shading Accent 4;
+\lsdpriority72 \lsdlocked0 Colorful List Accent 4;\lsdpriority73 \lsdlocked0 Colorful Grid Accent 4;\lsdpriority60 \lsdlocked0 Light Shading Accent 5;\lsdpriority61 \lsdlocked0 Light List Accent 5;\lsdpriority62 \lsdlocked0 Light Grid Accent 5;
+\lsdpriority63 \lsdlocked0 Medium Shading 1 Accent 5;\lsdpriority64 \lsdlocked0 Medium Shading 2 Accent 5;\lsdpriority65 \lsdlocked0 Medium List 1 Accent 5;\lsdpriority66 \lsdlocked0 Medium List 2 Accent 5;
+\lsdpriority67 \lsdlocked0 Medium Grid 1 Accent 5;\lsdpriority68 \lsdlocked0 Medium Grid 2 Accent 5;\lsdpriority69 \lsdlocked0 Medium Grid 3 Accent 5;\lsdpriority70 \lsdlocked0 Dark List Accent 5;\lsdpriority71 \lsdlocked0 Colorful Shading Accent 5;
+\lsdpriority72 \lsdlocked0 Colorful List Accent 5;\lsdpriority73 \lsdlocked0 Colorful Grid Accent 5;\lsdpriority60 \lsdlocked0 Light Shading Accent 6;\lsdpriority61 \lsdlocked0 Light List Accent 6;\lsdpriority62 \lsdlocked0 Light Grid Accent 6;
+\lsdpriority63 \lsdlocked0 Medium Shading 1 Accent 6;\lsdpriority64 \lsdlocked0 Medium Shading 2 Accent 6;\lsdpriority65 \lsdlocked0 Medium List 1 Accent 6;\lsdpriority66 \lsdlocked0 Medium List 2 Accent 6;
+\lsdpriority67 \lsdlocked0 Medium Grid 1 Accent 6;\lsdpriority68 \lsdlocked0 Medium Grid 2 Accent 6;\lsdpriority69 \lsdlocked0 Medium Grid 3 Accent 6;\lsdpriority70 \lsdlocked0 Dark List Accent 6;\lsdpriority71 \lsdlocked0 Colorful Shading Accent 6;
+\lsdpriority72 \lsdlocked0 Colorful List Accent 6;\lsdpriority73 \lsdlocked0 Colorful Grid Accent 6;\lsdqformat1 \lsdpriority19 \lsdlocked0 Subtle Emphasis;\lsdqformat1 \lsdpriority21 \lsdlocked0 Intense Emphasis;
+\lsdqformat1 \lsdpriority31 \lsdlocked0 Subtle Reference;\lsdqformat1 \lsdpriority32 \lsdlocked0 Intense Reference;\lsdqformat1 \lsdpriority33 \lsdlocked0 Book Title;\lsdsemihidden1 \lsdunhideused1 \lsdpriority37 \lsdlocked0 Bibliography;
+\lsdsemihidden1 \lsdunhideused1 \lsdqformat1 \lsdpriority39 \lsdlocked0 TOC Heading;\lsdpriority41 \lsdlocked0 Plain Table 1;\lsdpriority42 \lsdlocked0 Plain Table 2;\lsdpriority43 \lsdlocked0 Plain Table 3;\lsdpriority44 \lsdlocked0 Plain Table 4;
+\lsdpriority45 \lsdlocked0 Plain Table 5;\lsdpriority40 \lsdlocked0 Grid Table Light;\lsdpriority46 \lsdlocked0 Grid Table 1 Light;\lsdpriority47 \lsdlocked0 Grid Table 2;\lsdpriority48 \lsdlocked0 Grid Table 3;\lsdpriority49 \lsdlocked0 Grid Table 4;
+\lsdpriority50 \lsdlocked0 Grid Table 5 Dark;\lsdpriority51 \lsdlocked0 Grid Table 6 Colorful;\lsdpriority52 \lsdlocked0 Grid Table 7 Colorful;\lsdpriority46 \lsdlocked0 Grid Table 1 Light Accent 1;\lsdpriority47 \lsdlocked0 Grid Table 2 Accent 1;
+\lsdpriority48 \lsdlocked0 Grid Table 3 Accent 1;\lsdpriority49 \lsdlocked0 Grid Table 4 Accent 1;\lsdpriority50 \lsdlocked0 Grid Table 5 Dark Accent 1;\lsdpriority51 \lsdlocked0 Grid Table 6 Colorful Accent 1;
+\lsdpriority52 \lsdlocked0 Grid Table 7 Colorful Accent 1;\lsdpriority46 \lsdlocked0 Grid Table 1 Light Accent 2;\lsdpriority47 \lsdlocked0 Grid Table 2 Accent 2;\lsdpriority48 \lsdlocked0 Grid Table 3 Accent 2;
+\lsdpriority49 \lsdlocked0 Grid Table 4 Accent 2;\lsdpriority50 \lsdlocked0 Grid Table 5 Dark Accent 2;\lsdpriority51 \lsdlocked0 Grid Table 6 Colorful Accent 2;\lsdpriority52 \lsdlocked0 Grid Table 7 Colorful Accent 2;
+\lsdpriority46 \lsdlocked0 Grid Table 1 Light Accent 3;\lsdpriority47 \lsdlocked0 Grid Table 2 Accent 3;\lsdpriority48 \lsdlocked0 Grid Table 3 Accent 3;\lsdpriority49 \lsdlocked0 Grid Table 4 Accent 3;
+\lsdpriority50 \lsdlocked0 Grid Table 5 Dark Accent 3;\lsdpriority51 \lsdlocked0 Grid Table 6 Colorful Accent 3;\lsdpriority52 \lsdlocked0 Grid Table 7 Colorful Accent 3;\lsdpriority46 \lsdlocked0 Grid Table 1 Light Accent 4;
+\lsdpriority47 \lsdlocked0 Grid Table 2 Accent 4;\lsdpriority48 \lsdlocked0 Grid Table 3 Accent 4;\lsdpriority49 \lsdlocked0 Grid Table 4 Accent 4;\lsdpriority50 \lsdlocked0 Grid Table 5 Dark Accent 4;
+\lsdpriority51 \lsdlocked0 Grid Table 6 Colorful Accent 4;\lsdpriority52 \lsdlocked0 Grid Table 7 Colorful Accent 4;\lsdpriority46 \lsdlocked0 Grid Table 1 Light Accent 5;\lsdpriority47 \lsdlocked0 Grid Table 2 Accent 5;
+\lsdpriority48 \lsdlocked0 Grid Table 3 Accent 5;\lsdpriority49 \lsdlocked0 Grid Table 4 Accent 5;\lsdpriority50 \lsdlocked0 Grid Table 5 Dark Accent 5;\lsdpriority51 \lsdlocked0 Grid Table 6 Colorful Accent 5;
+\lsdpriority52 \lsdlocked0 Grid Table 7 Colorful Accent 5;\lsdpriority46 \lsdlocked0 Grid Table 1 Light Accent 6;\lsdpriority47 \lsdlocked0 Grid Table 2 Accent 6;\lsdpriority48 \lsdlocked0 Grid Table 3 Accent 6;
+\lsdpriority49 \lsdlocked0 Grid Table 4 Accent 6;\lsdpriority50 \lsdlocked0 Grid Table 5 Dark Accent 6;\lsdpriority51 \lsdlocked0 Grid Table 6 Colorful Accent 6;\lsdpriority52 \lsdlocked0 Grid Table 7 Colorful Accent 6;
+\lsdpriority46 \lsdlocked0 List Table 1 Light;\lsdpriority47 \lsdlocked0 List Table 2;\lsdpriority48 \lsdlocked0 List Table 3;\lsdpriority49 \lsdlocked0 List Table 4;\lsdpriority50 \lsdlocked0 List Table 5 Dark;
+\lsdpriority51 \lsdlocked0 List Table 6 Colorful;\lsdpriority52 \lsdlocked0 List Table 7 Colorful;\lsdpriority46 \lsdlocked0 List Table 1 Light Accent 1;\lsdpriority47 \lsdlocked0 List Table 2 Accent 1;\lsdpriority48 \lsdlocked0 List Table 3 Accent 1;
+\lsdpriority49 \lsdlocked0 List Table 4 Accent 1;\lsdpriority50 \lsdlocked0 List Table 5 Dark Accent 1;\lsdpriority51 \lsdlocked0 List Table 6 Colorful Accent 1;\lsdpriority52 \lsdlocked0 List Table 7 Colorful Accent 1;
+\lsdpriority46 \lsdlocked0 List Table 1 Light Accent 2;\lsdpriority47 \lsdlocked0 List Table 2 Accent 2;\lsdpriority48 \lsdlocked0 List Table 3 Accent 2;\lsdpriority49 \lsdlocked0 List Table 4 Accent 2;
+\lsdpriority50 \lsdlocked0 List Table 5 Dark Accent 2;\lsdpriority51 \lsdlocked0 List Table 6 Colorful Accent 2;\lsdpriority52 \lsdlocked0 List Table 7 Colorful Accent 2;\lsdpriority46 \lsdlocked0 List Table 1 Light Accent 3;
+\lsdpriority47 \lsdlocked0 List Table 2 Accent 3;\lsdpriority48 \lsdlocked0 List Table 3 Accent 3;\lsdpriority49 \lsdlocked0 List Table 4 Accent 3;\lsdpriority50 \lsdlocked0 List Table 5 Dark Accent 3;
+\lsdpriority51 \lsdlocked0 List Table 6 Colorful Accent 3;\lsdpriority52 \lsdlocked0 List Table 7 Colorful Accent 3;\lsdpriority46 \lsdlocked0 List Table 1 Light Accent 4;\lsdpriority47 \lsdlocked0 List Table 2 Accent 4;
+\lsdpriority48 \lsdlocked0 List Table 3 Accent 4;\lsdpriority49 \lsdlocked0 List Table 4 Accent 4;\lsdpriority50 \lsdlocked0 List Table 5 Dark Accent 4;\lsdpriority51 \lsdlocked0 List Table 6 Colorful Accent 4;
+\lsdpriority52 \lsdlocked0 List Table 7 Colorful Accent 4;\lsdpriority46 \lsdlocked0 List Table 1 Light Accent 5;\lsdpriority47 \lsdlocked0 List Table 2 Accent 5;\lsdpriority48 \lsdlocked0 List Table 3 Accent 5;
+\lsdpriority49 \lsdlocked0 List Table 4 Accent 5;\lsdpriority50 \lsdlocked0 List Table 5 Dark Accent 5;\lsdpriority51 \lsdlocked0 List Table 6 Colorful Accent 5;\lsdpriority52 \lsdlocked0 List Table 7 Colorful Accent 5;
+\lsdpriority46 \lsdlocked0 List Table 1 Light Accent 6;\lsdpriority47 \lsdlocked0 List Table 2 Accent 6;\lsdpriority48 \lsdlocked0 List Table 3 Accent 6;\lsdpriority49 \lsdlocked0 List Table 4 Accent 6;
+\lsdpriority50 \lsdlocked0 List Table 5 Dark Accent 6;\lsdpriority51 \lsdlocked0 List Table 6 Colorful Accent 6;\lsdpriority52 \lsdlocked0 List Table 7 Colorful Accent 6;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Mention;
+\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Smart Hyperlink;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Hashtag;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Unresolved Mention;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Smart Link;}}{\*\datastore 01050000
+02000000180000004d73786d6c322e534158584d4c5265616465722e362e3000000000000000000000060000
+d0cf11e0a1b11ae1000000000000000000000000000000003e000300feff090006000000000000000000000001000000010000000000000000100000feffffff00000000feffffff0000000000000000ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
+ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
+ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
+ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
+fffffffffffffffffdfffffffeffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
+ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
+ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
+ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
+ffffffffffffffffffffffffffffffff52006f006f007400200045006e00740072007900000000000000000000000000000000000000000000000000000000000000000000000000000000000000000016000500ffffffffffffffffffffffff0c6ad98892f1d411a65f0040963251e5000000000000000000000000609f
+e6fc39f7da01feffffff00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ffffffffffffffffffffffff00000000000000000000000000000000000000000000000000000000
+00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ffffffffffffffffffffffff0000000000000000000000000000000000000000000000000000
+000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ffffffffffffffffffffffff000000000000000000000000000000000000000000000000
+0000000000000000000000000000000000000000000000000105000000000000}}
\ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..3981b9d
--- /dev/null
+++ b/README.md
@@ -0,0 +1,34 @@
+# Brutatio
+
+A level editor for SMB1 Super Mario All-Stars
+
+## How to build
+
+Get Visual Studio
+
+## Questions
+
+Open an [Issue](https://github.com/Maseya/Brutario/issues) if you have
+any questions or bug reports.
+
+## Contributors
+
+[spel werdz rite](https://github.com/bonimy): Main (only) programmer
+
+[Ersanio](https://twitter.com/Ersanio): SMASDis author, friend who kept
+me going.
+
+[Alcaro](https://github.com/Alcaro/): Bug tester and Linux bully (yes
+I'll add Linux support at some point).
+
+The whole SMASDIS team
+
+There are others. If I forgot you, please let me know.
+
+### Communities that kept me strong
+
+[Snes Lab](https://discord.gg/8GkqjWWkEk)
+
+[snesdev](https://discord.gg/AJagHMpZ)
+
+[SMW Central](https://www.smwcentral.net/?p=main) ([Discord](https://discord.gg/smwc))
diff --git a/src/Brutario.Core/AreaEditor.cs b/src/Brutario.Core/AreaEditor.cs
new file mode 100644
index 0000000..797b816
--- /dev/null
+++ b/src/Brutario.Core/AreaEditor.cs
@@ -0,0 +1,292 @@
+namespace Brutario.Core;
+
+using System;
+
+using Maseya.Smas.Smb1;
+using Maseya.Smas.Smb1.AreaData;
+using Maseya.Smas.Smb1.AreaData.HeaderData;
+using Maseya.Snes;
+
+public class AreaEditor
+{
+ public const int ScreenCount = 0x20;
+ public const int ScreenWidth = 0x10;
+ public const int ScreenHeight = 0x10;
+ public const int TilemapWidth = ScreenCount * ScreenWidth;
+ public const int TileMapLength = TilemapWidth * ScreenHeight;
+
+ private AreaHeader _areaHeader;
+
+ private Player _player;
+
+ private PlayerState _playerState;
+
+ public AreaEditor(
+ GameData gameData,
+ int areaNumber)
+ {
+ GameData = gameData;
+ AreaNumber = areaNumber;
+
+ Palette = new Color32BppArgb[0x140];
+ PixelData = new byte[GfxData.TotalPixelDataSize];
+ Map16Tiles = new Obj16Tile[0x100];
+ TileMap = new int[TileMapLength];
+ BG1 = new ObjTile[TileMapLength * 4];
+
+ UndoFactory = new UndoFactory();
+
+ GameData.GfxData.ReadStaticData(PixelData);
+ GameData.Map16Data.ReadStaticTiles(Map16Tiles);
+
+ AreaHeader = GameData.AreaLoader.Headers[ObjectAreaIndex];
+ ObjectDataInternal = new SortedObjectListEditor(
+ GameData.AreaLoader.AreaObjectData[ObjectAreaIndex]);
+ SpriteDataInternal = new SortedSpriteListEditor(
+ GameData.AreaLoader.AreaSpriteData[SpriteAreaIndex]);
+
+ ReloadPaletteInternal();
+ GameData!.TilemapLoader.LoadTilemap(ObjectAreaIndex);
+ ReloadGfxInternal();
+
+ RenderAreaTilemapInternal();
+
+ }
+
+ public event EventHandler? AreaHeaderChanged;
+
+ public event EventHandler? PlayerChanged;
+
+ public event EventHandler? PlayerStateChanged;
+
+ public PlayerState PlayerState
+ {
+ get
+ {
+ return _playerState;
+ }
+
+ set
+ {
+ if (PlayerState == value)
+ {
+ return;
+ }
+
+ _playerState = value;
+ OnPlayerStateChanged(EventArgs.Empty);
+ }
+ }
+
+ public Player Player
+ {
+ get
+ {
+ return _player;
+ }
+
+ set
+ {
+ if (Player == value)
+ {
+ return;
+ }
+
+ _player = value;
+ OnPlayerChanged(EventArgs.Empty);
+ }
+ }
+
+ public int AreaNumber
+ {
+ get;
+ }
+
+ public AreaHeader AreaHeader
+ {
+ get
+ {
+ return _areaHeader;
+ }
+
+ set
+ {
+ if (AreaHeader == value)
+ {
+ return;
+ }
+
+ _areaHeader = value;
+ OnAreaHeaderChanged(EventArgs.Empty);
+ }
+ }
+
+ private SortedObjectListEditor ObjectDataInternal
+ {
+ get;
+ }
+
+ private SortedSpriteListEditor SpriteDataInternal
+ {
+ get;
+ }
+
+ private Color32BppArgb[] Palette
+ {
+ get;
+ }
+
+ private byte[] PixelData
+ {
+ get;
+ }
+
+ private Obj16Tile[] Map16Tiles
+ {
+ get;
+ }
+
+ private int[] TileMap
+ {
+ get;
+ }
+
+ private ObjTile[] BG1
+ {
+ get;
+ }
+
+ private AreaType AreaType
+ {
+ get
+ {
+ return (AreaType)((AreaNumber & 0x7F) >> 5);
+ }
+ }
+
+ private int ObjectAreaIndex
+ {
+ get
+ {
+ return GameData!.AreaLoader.GetObjectAreaIndex(AreaNumber);
+ }
+ }
+
+ private int SpriteAreaIndex
+ {
+ get
+ {
+ return GameData!.AreaLoader.GetSpriteAreaIndex(AreaNumber);
+ }
+ }
+
+ private UndoFactory UndoFactory
+ {
+ get;
+ }
+
+ private int SaveHistoryIndex
+ {
+ get;
+ set;
+ }
+
+ private int CurrentHistoryIndex
+ {
+ get;
+ set;
+ }
+
+ private bool IsAreaLoaded { get; set; }
+
+ private GameData GameData
+ {
+ get;
+ }
+
+ protected virtual void OnPlayerStateChanged(EventArgs e)
+ {
+ PlayerStateChanged?.Invoke(this, e);
+ }
+
+ protected virtual void OnPlayerChanged(EventArgs e)
+ {
+ PlayerChanged?.Invoke(this, e);
+ }
+
+ protected virtual void OnAreaHeaderChanged(EventArgs e)
+ {
+ AreaHeaderChanged?.Invoke(this, e);
+ }
+
+ private void ReloadPaletteInternal()
+ {
+ // TODO(nrg): Change AreaNumber requirement to tileset requirement.
+ var isLuigiBonusArea = Player == Player.Luigi
+ && (AreaNumber == 0x42 || AreaNumber == 0x2B);
+ GameData.PaletteData.ReadPalette(
+ ObjectAreaIndex,
+ isLuigiBonusArea,
+ Player,
+ PlayerState,
+ Palette);
+ }
+
+ private void ReloadGfxInternal()
+ {
+ GameData.GfxData.ReadAreaTileSet(
+ ObjectAreaIndex,
+ GameData.TilemapLoader.TileSetIndex,
+ Player,
+ PixelData);
+ }
+
+ private void RenderAreaTilemapInternal()
+ {
+ GameData!.AreaObjectRenderer.RenderTileMap(
+ TileMap,
+ AreaType,
+ AreaHeader,
+ ObjectDataInternal.GetObjectData().ToArray(),
+ AreaNumber == 2);
+ ReadBG1Tiles();
+ }
+
+ private void ReadBG1Tiles()
+ {
+ const int width = 0x200;
+ var tiles = TileMap;
+ var height = tiles.Length / width;
+ for (var y = 0; y < height; y++)
+ {
+ var srcRow = y * width;
+ var destRow = srcRow << 2;
+ for (var srcX = 0; srcX < width; srcX++)
+ {
+ var destX = srcX << 1;
+ var index = srcRow + srcX;
+ var tileIndex = (byte)tiles[index];
+ var tile = Map16Tiles[tileIndex];
+ if (tileIndex is 0x56 or 0x57)
+ {
+ if (srcX > 0 && ((byte)tiles[index - 1]) == 0)
+ {
+ tile.TopLeft += 4;
+ tile.BottomLeft += 4;
+ }
+
+ if (srcX + 1 < width && ((byte)tiles[index + 1]) == 0)
+ {
+ tile.TopRight += 4;
+ tile.BottomRight += 4;
+ }
+ }
+
+ BG1[destRow + destX] = tile.TopLeft;
+ BG1[destRow + destX + 1] = tile.TopRight;
+ BG1[destRow + (width << 1) + destX] = tile.BottomLeft;
+ BG1[destRow + (width << 1) + destX + 1] = tile.BottomRight;
+ }
+ }
+ }
+}
diff --git a/src/Brutario.Core/AutoSaveHelper.cs b/src/Brutario.Core/AutoSaveHelper.cs
new file mode 100644
index 0000000..07843bf
--- /dev/null
+++ b/src/Brutario.Core/AutoSaveHelper.cs
@@ -0,0 +1,118 @@
+namespace Brutario.Core;
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+
+public static class AutoSaveHelper
+{
+ public const string DateTimeFormat = "yyyy\"-\"MM\"-\"dd\"T\"HH\"h\"mm\"m\"ss\"s\"";
+
+ public static readonly CultureInfo FormatCulture = CultureInfo.InvariantCulture;
+
+ public static void AutoSave(string basePath, byte[] data)
+ {
+ var dir = Path.GetDirectoryName(basePath) ?? String.Empty;
+ var ext = Path.GetExtension(basePath) ?? String.Empty;
+
+ var timeStamp = DateTime.Now.ToUniversalTime().ToString(
+ DateTimeFormat,
+ FormatCulture);
+
+ var dest = Path.Combine(
+ dir,
+ timeStamp + ext);
+
+ try
+ {
+ if (!Directory.Exists(dir))
+ {
+ _ = Directory.CreateDirectory(dir);
+ }
+
+ File.WriteAllBytes(dest, data);
+ }
+ catch (IOException ex)
+ {
+ Console.WriteLine(ex.ToString());
+ }
+ }
+
+ public static void PruneOldAutoSaves(string basePath, TimeSpan ago, bool constant = false)
+ {
+ var dir = Path.GetDirectoryName(basePath) ?? String.Empty;
+ var files = Directory.GetFiles(dir);
+ var items = new SortedDictionary();
+
+ foreach (var file in files)
+ {
+ var utcTime = GetDateTime(basePath, file);
+ if (!utcTime.HasValue)
+ {
+ continue;
+ }
+
+ var localTime = utcTime.Value.ToLocalTime();
+ items.Add(localTime, file);
+ }
+
+ if (items.Count == 0)
+ {
+ return;
+ }
+
+ var cutoff = items.Last().Key - ago;
+ using var en = items.GetEnumerator();
+ while (en.MoveNext())
+ {
+ if (en.Current.Key < cutoff)
+ {
+ var skip = 0;
+ do
+ {
+ for (var i = 0; i < skip; i++)
+ {
+ if (!en.MoveNext())
+ {
+ return;
+ }
+ }
+
+ if (!constant)
+ {
+ skip++;
+ }
+
+ File.Delete(en.Current.Value);
+ } while (en.MoveNext());
+ }
+ }
+ }
+
+ private static DateTime? GetDateTime(string basePath, string actualPath)
+ {
+ var dir = Path.GetDirectoryName(basePath) ?? String.Empty;
+ var ext = Path.GetExtension(basePath) ?? String.Empty;
+
+ var comparer = StringComparer.OrdinalIgnoreCase;
+ if (!comparer.Equals(dir, Path.GetDirectoryName(actualPath)))
+ {
+ return null;
+ }
+
+ if (!comparer.Equals(ext, Path.GetExtension(actualPath)))
+ {
+ return null;
+ }
+
+ var actualName = Path.GetFileNameWithoutExtension(actualPath);
+ return DateTime.TryParseExact(
+ actualName,
+ DateTimeFormat,
+ FormatCulture,
+ DateTimeStyles.None,
+ out var date)
+ ? date
+ : null;
+ }
+
+}
diff --git a/src/Brutario.Core/Brutario.Core.csproj b/src/Brutario.Core/Brutario.Core.csproj
new file mode 100644
index 0000000..f0e2e90
--- /dev/null
+++ b/src/Brutario.Core/Brutario.Core.csproj
@@ -0,0 +1,19 @@
+
+
+
+ net8.0
+ enable
+ enable
+ en-US
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Brutario.Core/BrutarioEditor.cs b/src/Brutario.Core/BrutarioEditor.cs
new file mode 100644
index 0000000..d30271a
--- /dev/null
+++ b/src/Brutario.Core/BrutarioEditor.cs
@@ -0,0 +1,1945 @@
+//
+// Copyright (c) 2022 spel werdz rite. All rights reserved. Licensed under GNU Affero
+// General Public License. See LICENSE in project root for full license
+// information, or visit https://www.gnu.org/licenses/#AGPL
+//
+
+namespace Brutario.Core;
+
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Drawing;
+using System.IO;
+using System.Linq;
+
+using Brutario.Core.Views;
+
+using Maseya.Smas.Smb1;
+using Maseya.Smas.Smb1.AreaData;
+using Maseya.Smas.Smb1.AreaData.HeaderData;
+using Maseya.Smas.Smb1.AreaData.ObjectData;
+using Maseya.Smas.Smb1.AreaData.SpriteData;
+using Maseya.Snes;
+
+public class BrutarioEditor : IMainEditor
+{
+ public const int ScreenCount = 0x20;
+ public const int ScreenWidth = 0x10;
+ public const int ScreenHeight = 0x10;
+ public const int TilemapWidth = ScreenCount * ScreenWidth;
+ public const int TileMapLength = TilemapWidth * ScreenHeight;
+
+ private string _path = String.Empty;
+
+ private int _areaNumber;
+
+ private AreaHeader _areaHeader;
+
+ private int _selectedObjectIndex;
+
+ private int _selectedSpriteIndex;
+
+ private int _startX;
+
+ private bool _spriteMode;
+
+ private Player _player;
+
+ private PlayerState _playerState;
+
+ private int _animationFrame;
+
+ private int _saveHistoryIndex;
+
+ private int _currentHistoryIndex;
+
+ private bool _hasUnsavedChanges;
+
+ private UIAreaObjectCommand? _copiedObject;
+
+ private UIAreaSpriteCommand? _copiedSprite;
+
+ public BrutarioEditor()
+ {
+ Path = String.Empty;
+ Player = Player.Mario;
+ PlayerState = PlayerState.Big;
+
+ Palette = new Color32BppArgb[0x140];
+ PixelData = new byte[GfxData.TotalPixelDataSize];
+ Map16Tiles = new Obj16Tile[0x100];
+ TileMap = new int[TileMapLength];
+ BG1 = new ObjTile[TileMapLength * 4];
+
+ ObjectData = new SortedObjectListEditor();
+ ObjectData.DataReset += (s, e) => OnObjectData_DataReset(e);
+ ObjectData.ItemEdited += (s, e) => OnObjectData_ItemEdited(e);
+ ObjectData.ItemAdded += (s, e) => OnObjectData_ItemAdded(e);
+ ObjectData.ItemRemoved += (s, e) => OnObjectData_ItemRemoved(e);
+ ObjectData.DataCleared += (s, e) => OnObjectData_DataCleared(e);
+
+ SpriteData = new SortedSpriteListEditor();
+
+ UndoFactory = new UndoFactory();
+ UndoFactory.Cleared += UndoFactory_Cleared;
+ UndoFactory.UndoElementAdded += UndoFactory_UndoElementAdded;
+ UndoFactory.UndoComplete += UndoFactory_UndoComplete;
+ UndoFactory.RedoComplete += UndoFactory_RedoComplete;
+ }
+
+ public event EventHandler? PathChanged;
+
+ public event EventHandler? FileOpened;
+
+ public event EventHandler? FileSaved;
+
+ public event EventHandler? FileClosed;
+
+ public event EventHandler? HasUnsavedChangesChanged;
+
+ public event EventHandler? HistoryCleared;
+
+ public event EventHandler? UndoElementAdded;
+
+ public event EventHandler? UndoComplete;
+
+ public event EventHandler? RedoComplete;
+
+ public event EventHandler? ItemCopied;
+
+ public event EventHandler? CopiedSpriteChanged;
+
+ public event EventHandler? CopiedObjectChanged;
+
+ public event EventHandler? AreaNumberChanged;
+
+ public event EventHandler? AreaLoaded;
+
+ public event EventHandler? AreaHeaderChanged;
+
+ public event EventHandler? SelectedObjectChanged;
+
+ public event EventHandler? SelectedSpriteChanged;
+
+ public event EventHandler? StartXChanged;
+
+ public event EventHandler? SpriteModeChanged;
+
+ public event EventHandler? PlayerChanged;
+
+ public event EventHandler? PlayerStateChanged;
+
+ public event EventHandler? Invalidated;
+
+ public event EventHandler? ObjectData_DataReset;
+
+ public event EventHandler? ObjectData_ItemEdited;
+
+ public event EventHandler? ObjectData_ItemAdded;
+
+ public event EventHandler? ObjectData_ItemRemoved;
+
+ public event EventHandler? ObjectData_DataCleared;
+
+ public event EventHandler? AnimationFrameChanged;
+
+ public bool EditSelectedObjectEnabled { get; set; }
+
+ public bool EditSelectedSpriteEnabled { get; set; }
+
+ public int SelectedObjectIndex
+ {
+ get
+ {
+ return _selectedObjectIndex;
+ }
+
+ set
+ {
+ if (SelectedObjectIndex == value)
+ {
+ return;
+ }
+
+ _selectedObjectIndex = value;
+ OnSelectedObjectChanged(EventArgs.Empty);
+ }
+ }
+
+ public UIAreaObjectCommand SelectedObject
+ {
+ get
+ {
+ return ObjectData[SelectedObjectIndex];
+ }
+ }
+
+ public UIAreaObjectCommand DefaultPreviewObject
+ {
+ get
+ {
+ var command = default(AreaObjectCommand);
+ command.X = PreferredX & 0x0F;
+ command.Y = PreferredY;
+ return new UIAreaObjectCommand(command, PreferredX >> 4);
+ }
+ }
+
+ public int SelectedSpriteIndex
+ {
+ get
+ {
+ return _selectedSpriteIndex;
+ }
+
+ set
+ {
+ if (SelectedSpriteIndex == value)
+ {
+ return;
+ }
+
+ _selectedSpriteIndex = value;
+ OnSelectedSpriteChanged(EventArgs.Empty);
+ }
+ }
+
+ public UIAreaSpriteCommand SelectedSprite
+ {
+ get
+ {
+ return SpriteData[SelectedSpriteIndex];
+ }
+ }
+
+ public UIAreaSpriteCommand DefaultPreviewSprite
+ {
+ get
+ {
+ var command = default(AreaSpriteCommand);
+ command.X = PreferredX & 0x0F;
+ command.Y = PreferredY;
+ return new UIAreaSpriteCommand(command, PreferredX >> 4);
+ }
+ }
+
+ public UIAreaObjectCommand? CopiedObject
+ {
+ get
+ {
+ return _copiedObject;
+ }
+
+ set
+ {
+ if (CopiedObject == value)
+ {
+ return;
+ }
+
+ _copiedObject = value;
+ OnCopiedObjectChanged(EventArgs.Empty);
+ }
+ }
+
+ public UIAreaSpriteCommand? CopiedSprite
+ {
+ get
+ {
+ return _copiedSprite;
+ }
+
+ set
+ {
+ if (CopiedSprite == value)
+ {
+ return;
+ }
+
+ _copiedSprite = value;
+ OnCopiedSpriteChanged(EventArgs.Empty);
+ }
+ }
+
+ public string Path
+ {
+ get
+ {
+ return _path;
+ }
+
+ private set
+ {
+ if (Path == value)
+ {
+ return;
+ }
+
+ _path = value;
+ OnPathChanged(EventArgs.Empty);
+ }
+ }
+
+ public bool IsOpen
+ {
+ get
+ {
+ return Rom is not null;
+ }
+ }
+
+ public bool HasUnsavedChanges
+ {
+ get
+ {
+ return _hasUnsavedChanges;
+ }
+
+ private set
+ {
+ if (HasUnsavedChanges == value)
+ {
+ return;
+ }
+
+ _hasUnsavedChanges = value;
+ OnHasUnsavedChangesChanged(EventArgs.Empty);
+ }
+ }
+
+ public bool CanUndo
+ {
+ get
+ {
+ return UndoFactory.CanUndo;
+ }
+ }
+
+ public bool CanRedo
+ {
+ get
+ {
+ return UndoFactory.CanRedo;
+ }
+ }
+
+ public int AreaNumber
+ {
+ get
+ {
+ return _areaNumber;
+ }
+
+ set
+ {
+ if (GameData is null)
+ {
+ throw new InvalidOperationException();
+ }
+
+ if (AreaNumber == value)
+ {
+ return;
+ }
+
+ if (value < 0)
+ {
+ throw new ArgumentOutOfRangeException(
+ nameof(AreaNumber),
+ "Area number cannot be negative");
+ }
+
+ if (!GameData.AreaLoader.IsValidAreaNumberForObjectData(value))
+ {
+ throw new ArgumentOutOfRangeException(
+ nameof(AreaNumber),
+ "Area number points to an object area index that would be invalid.");
+ }
+
+ if (!GameData.AreaLoader.IsValidAreaNumberForSpriteData(value))
+ {
+ throw new ArgumentOutOfRangeException(
+ nameof(AreaNumber),
+ "Area number points to a sprite area index that would be invalid.");
+ }
+
+ SetAreaNumberInternal(
+ oldAreaNumber: AreaNumber,
+ newAreaNumber: value);
+ }
+ }
+
+ public AreaHeader AreaHeader
+ {
+ get
+ {
+ return _areaHeader;
+ }
+
+ set
+ {
+ if (AreaHeader == value)
+ {
+ return;
+ }
+
+ SetAreaHeaderInternal(
+ oldAreaHeader: AreaHeader,
+ newAreaHeader: value);
+ }
+ }
+
+ public int StartX
+ {
+ get
+ {
+ return _startX;
+ }
+
+ set
+ {
+ if (StartX == value)
+ {
+ return;
+ }
+
+ _startX = value;
+ OnStartXChanged(EventArgs.Empty);
+ }
+ }
+
+ public bool SpriteMode
+ {
+ get
+ {
+ return _spriteMode;
+ }
+
+ set
+ {
+ if (SpriteMode == value)
+ {
+ return;
+ }
+
+ _spriteMode = value;
+ OnSpriteModeChanged(EventArgs.Empty);
+ }
+ }
+
+ public Player Player
+ {
+ get
+ {
+ return _player;
+ }
+
+ set
+ {
+ if (Player == value)
+ {
+ return;
+ }
+
+ _player = value;
+ OnPlayerChanged(EventArgs.Empty);
+ }
+ }
+
+ public PlayerState PlayerState
+ {
+ get
+ {
+ return _playerState;
+ }
+
+ set
+ {
+ if (PlayerState == value)
+ {
+ return;
+ }
+
+ _playerState = value;
+ OnPlayerStateChanged(EventArgs.Empty);
+ }
+ }
+
+ public int AnimationFrame
+ {
+ get
+ {
+ return _animationFrame;
+ }
+
+ set
+ {
+ if (AnimationFrame == value)
+ {
+ return;
+ }
+
+ _animationFrame = value;
+ OnAnimationFrameChanged(EventArgs.Empty);
+ }
+ }
+
+ public SortedObjectListEditor ObjectData
+ {
+ get;
+ }
+
+ public SortedSpriteListEditor SpriteData
+ {
+ get;
+ }
+
+ IReadOnlyList IMainEditor.ObjectData
+ {
+ get
+ {
+ return ObjectData;
+ }
+ }
+
+ IReadOnlyList IMainEditor.SpriteData
+ {
+ get
+ {
+ return SpriteData;
+ }
+ }
+
+ private Rom? Rom
+ {
+ get;
+ set;
+ }
+
+ private AreaType AreaType
+ {
+ get
+ {
+ return (AreaType)((AreaNumber & 0x7F) >> 5);
+ }
+ }
+
+ private int ObjectAreaIndex
+ {
+ get
+ {
+ return GameData!.AreaLoader.GetObjectAreaIndex(AreaNumber);
+ }
+ }
+
+ private int SpriteAreaIndex
+ {
+ get
+ {
+ return GameData!.AreaLoader.GetSpriteAreaIndex(AreaNumber);
+ }
+ }
+
+ private Color32BppArgb[] Palette
+ {
+ get;
+ }
+
+ private byte[] PixelData
+ {
+ get;
+ }
+
+ private Obj16Tile[] Map16Tiles
+ {
+ get;
+ }
+
+ private int[] TileMap
+ {
+ get;
+ }
+
+ private ObjTile[] BG1
+ {
+ get;
+ }
+
+ private bool IsAreaLoaded
+ {
+ get;
+ set;
+ }
+
+ private UndoFactory UndoFactory
+ {
+ get;
+ }
+
+ private int SaveHistoryIndex
+ {
+ get
+ {
+ return _saveHistoryIndex;
+ }
+ set
+ {
+ _saveHistoryIndex = value;
+ HasUnsavedChanges = SaveHistoryIndex != CurrentHistoryIndex;
+ }
+ }
+
+ private int CurrentHistoryIndex
+ {
+ get
+ {
+ return _currentHistoryIndex;
+ }
+ set
+ {
+ _currentHistoryIndex = value;
+ HasUnsavedChanges = SaveHistoryIndex != CurrentHistoryIndex;
+ }
+ }
+
+ private int OldObjectIndex { get; set; }
+
+ private UIAreaObjectCommand OldObject { get; set; }
+
+ private int OldSpriteIndex { get; set; }
+
+ private UIAreaSpriteCommand OldSprite { get; set; }
+
+ private int PreferredX { get; set; }
+
+ private int PreferredY { get; set; }
+
+ private GameData? GameData
+ {
+ get; set;
+ }
+
+ private string? AutoSavePath { get; set; }
+
+ public bool AutoSaveEnabled { get; set; }
+
+ public bool PruneAutoSavesEnabled { get; set; }
+
+ public TimeSpan AutoSaveInterval { get; set; }
+
+ public TimeSpan AutoSaveCutoffAge { get; set; }
+
+ public bool AutoSaveHardCutoff { get; set; }
+
+ private DateTime LastAutoSaveTime { get; set; }
+
+ private byte[]? LastAutoSaveData { get; set; }
+
+ public void Open(string path)
+ {
+ // We initialize these as locals first so that if either throws, we don't mess
+ // up the last valid state of the editor. Making this its own struct/class may
+ // be a good idea.
+ var rom = new Rom(File.ReadAllBytes(path));
+ var gameData = new GameData(rom);
+
+ // These should never throw.
+ Rom = rom;
+ Path = path;
+ GameData = gameData;
+ GameData.GfxData.ReadStaticData(PixelData);
+ GameData.Map16Data.ReadStaticTiles(Map16Tiles);
+
+ // Internally, these should never throw. However, they call events, which we
+ // cannot control. If an event throws, it could mess up our state. But if this
+ // is true, then it becomes the responsibility of the event caller to fix this.
+ OnFileOpened(EventArgs.Empty);
+ SetAreaNumberInternal(
+ oldAreaNumber: 0,
+ newAreaNumber: GameData.AreaLoader.GetAreaNumber(world: 0, level: 0),
+ discardHistory: true);
+ }
+
+ public void Save()
+ {
+ SaveAs(Path);
+ }
+
+ public void SaveAs(string path)
+ {
+ if (Rom is null)
+ {
+ throw new InvalidOperationException();
+ }
+
+ WriteToGameData();
+ File.WriteAllBytes(path, Rom.GetData());
+ Path = path;
+ OnFileSaved(EventArgs.Empty);
+ }
+
+ public void TryAutoSave()
+ {
+ if (!AutoSaveEnabled)
+ {
+ return;
+ }
+
+ if (Rom is null)
+ {
+ throw new InvalidOperationException();
+ }
+
+ if (DateTime.Now.ToUniversalTime() - LastAutoSaveTime < AutoSaveInterval)
+ {
+ return;
+ }
+
+ // TODO(swr): This may need to be asynchronous
+ WriteToGameData();
+
+ var data = Rom.GetData();
+ if (LastAutoSaveData is not null
+ && Enumerable.SequenceEqual(data, LastAutoSaveData))
+ {
+ return;
+ }
+
+ AutoSaveHelper.AutoSave(AutoSavePath!, data);
+ LastAutoSaveData = data;
+
+ PruneOldAutoSaves();
+ }
+
+ private void PruneOldAutoSaves()
+ {
+ if (!PruneAutoSavesEnabled)
+ {
+ return;
+ }
+
+ if (Rom is null)
+ {
+ throw new InvalidOperationException();
+ }
+
+ AutoSaveHelper.PruneOldAutoSaves(AutoSavePath!, AutoSaveCutoffAge, AutoSaveHardCutoff);
+ }
+
+ public void Close()
+ {
+ Path = String.Empty;
+ Rom = null;
+ ClearHistory();
+ FileClosed?.Invoke(this, EventArgs.Empty);
+ }
+
+ public bool IsValidAreaNumber(int areaNumber)
+ {
+ return GameData is not null
+ && GameData.AreaLoader.IsValidAreaNumberForObjectData(areaNumber)
+ && GameData.AreaLoader.IsValidAreaNumberForSpriteData(areaNumber);
+ }
+
+ public void Undo()
+ {
+ UndoFactory.Undo();
+ }
+
+ public void Redo()
+ {
+ UndoFactory.Redo();
+ }
+
+ public void CutCurrentItem()
+ {
+ CopyCurrentItem();
+ RemoveCurrentItem();
+ }
+
+ public void CopyCurrentItem()
+ {
+ if (SpriteMode)
+ {
+ CopiedSprite = SelectedSpriteIndex != -1
+ ? SelectedSprite
+ : throw new InvalidOperationException();
+ }
+ else
+ {
+ CopiedObject = SelectedObjectIndex != -1
+ ? SelectedObject
+ : throw new InvalidOperationException();
+ }
+ }
+
+ public void Paste()
+ {
+ Paste(PreferredX, PreferredY);
+ }
+
+ public void Paste(int x, int y)
+ {
+ if (SpriteMode)
+ {
+ if (CopiedSprite is null)
+ {
+ throw new InvalidOperationException();
+ }
+
+ var sprite = (UIAreaSpriteCommand)CopiedSprite!;
+ sprite.X = x;
+ sprite.Y = y;
+ SpriteData.Add(sprite);
+ throw new InvalidOperationException("ObjectData.Add is not preffered. Use AddObjectInternal");
+ }
+ else
+ {
+ if (CopiedObject is null)
+ {
+ throw new InvalidOperationException();
+ }
+
+ var item = (UIAreaObjectCommand)CopiedObject!;
+ item.X = x;
+ item.Y = y;
+ SelectedObjectIndex = AddObjectInternal(item);
+ }
+ }
+
+ public void AddPreviewObject(UIAreaObjectCommand item)
+ {
+ // We add an object to the object list, but do not persist its changes
+ // to the history. The user may be changing the object properties a lot
+ // as they design the object to their liking, and we don't want all of
+ // this in the history, just whatever the final product is. There's lots
+ // of rooms for bugs in this command, as we change the state of the
+ // editor pretty hard. We can't add multiple preview objects at once, we
+ // can't switch to sprite, we probably shouldn't allow edits to other
+ // things such as header or other area data. If the user decides to go
+ // to another area, we need to exit gracefully from preview object mode.
+ // Overall, just be careful with this command, as it makes things very
+ // different.
+ SelectedObjectIndex = AddObjectInternal(item, discardHistory: true);
+ }
+
+ public void BeginEditObject()
+ {
+ OldObjectIndex = SelectedObjectIndex;
+ OldObject = SelectedObject;
+ }
+
+ public void EditPreviewObject(UIAreaObjectCommand newItem)
+ {
+ SelectedObjectIndex = EditObjectInternal(
+ SelectedObjectIndex, newItem, discardHistory: true);
+ }
+
+ public void FinishAddObject(bool commit)
+ {
+ if (commit)
+ {
+ PushObjectAdded(SelectedObjectIndex);
+ }
+ else
+ {
+ RemoveObjectInternal(SelectedObjectIndex, discardHistory: true);
+ }
+ }
+
+ public void AddPreviewSprite(UIAreaSpriteCommand item)
+ {
+ AddSpriteInternal(item, discardHistory: true);
+ }
+
+ public void EditPreviewSprite(
+ UIAreaSpriteCommand newItem)
+ {
+ SelectedSpriteIndex = EditSpriteInternal(
+ SelectedSpriteIndex, newItem, discardHistory: true);
+ }
+
+ public void FinishAddSprite(bool commit)
+ {
+ if (commit)
+ {
+ PushSpriteAdded(SelectedSpriteIndex);
+ }
+ else
+ {
+ RemoveSpriteInternal(SelectedSpriteIndex, discardHistory: true);
+ }
+ }
+
+ public void RemoveCurrentItem()
+ {
+ if (SpriteMode)
+ {
+ if (SelectedSpriteIndex != -1)
+ {
+ RemoveSpriteInternal(SelectedSpriteIndex);
+ }
+ else
+ {
+ throw new InvalidOperationException();
+ }
+ }
+ else
+ {
+ if (SelectedObjectIndex != -1)
+ {
+ RemoveObjectInternal(SelectedObjectIndex);
+ }
+ else
+ {
+ throw new InvalidOperationException();
+ }
+ }
+ }
+
+ public void RemoveObject(UIAreaObjectCommand item)
+ {
+ var index = ObjectData.IndexOf(item);
+ if (index != -1)
+ {
+ RemoveObjectInternal(index);
+ }
+ }
+
+ public void RemoveObjectAt(int index)
+ {
+ RemoveObjectInternal(index);
+ }
+
+ public void RemoveSprite(int index)
+ {
+ RemoveSpriteInternal(index);
+ }
+
+ public void RemoveAllItems()
+ {
+ if (SpriteMode)
+ {
+ ClearSprites();
+ }
+ else
+ {
+ ClearObjects();
+ }
+ }
+
+ public void ClearObjects()
+ {
+ ClearObjectsInternal();
+ }
+
+ public void ClearSprites()
+ {
+ ClearSpritesInternal();
+ }
+
+ public byte[] ExportTileMap()
+ {
+ var data = new byte[0x20 * 0x10 * 0x0D];
+ for (var i = 0; i < data.Length; i++)
+ {
+ data[i] = (byte)TileMap[i + (2 * 0x20 * 0x10)];
+ }
+
+ return data;
+ }
+
+ public void InitializeSetAreaHeader(AreaHeader newAreaHeader)
+ {
+ SetAreaHeaderInternal(default, newAreaHeader, discardHistory: true);
+ }
+
+ public void FinishSetAreaHeader(AreaHeader oldAreaHeader, bool commit)
+ {
+ if (!commit)
+ {
+ SetAreaHeaderInternal(
+ oldAreaHeader: AreaHeader,
+ newAreaHeader: oldAreaHeader,
+ discardHistory: true);
+ }
+ else if (oldAreaHeader != AreaHeader)
+ {
+ PushAreaHeaderChanged(
+ oldAreaHeader: oldAreaHeader,
+ newAreaHeader: AreaHeader);
+ }
+ }
+
+ public void InitializeEditItem()
+ {
+ if (SpriteMode)
+ {
+ InitializeEditSprite();
+ }
+ else
+ {
+ InitializeEditObject();
+ }
+ }
+
+ public void InitializeEditObject()
+ {
+ // Do not override initialization if one already exists.
+ if (EditSelectedObjectEnabled)
+ {
+ return;
+ }
+
+ if (SelectedObjectIndex != -1)
+ {
+ OldObjectIndex = SelectedObjectIndex;
+ OldObject = SelectedObject;
+ EditSelectedObjectEnabled = true;
+ }
+ }
+
+ private void InitializeEditSprite()
+ {
+ if (EditSelectedSpriteEnabled)
+ {
+ return;
+ }
+
+ if (SelectedSpriteIndex != -1)
+ {
+ OldSpriteIndex = SelectedSpriteIndex;
+ OldSprite = SelectedSprite;
+ EditSelectedSpriteEnabled = true;
+
+ }
+ }
+
+ public void FinishEditItem(bool commit)
+ {
+ if (SpriteMode)
+ {
+ FinishEditSprite(commit);
+ }
+ else
+ {
+ FinishEditObject(commit);
+ }
+ }
+
+ public void FinishEditObject(bool commit)
+ {
+ if (!EditSelectedObjectEnabled || SelectedObjectIndex == -1 || OldObjectIndex == -1)
+ {
+ return;
+ }
+
+ if (!commit)
+ {
+ SelectedObjectIndex = EditObjectInternal(
+ SelectedObjectIndex,
+ OldObject,
+ discardHistory: true);
+ }
+ else if (OldObject != SelectedObject)
+ {
+ PushObjectEdited(
+ oldIndex: OldObjectIndex,
+ oldItem: OldObject,
+ newIndex: SelectedObjectIndex,
+ newItem: SelectedObject);
+ }
+
+ OldObjectIndex = -1;
+ EditSelectedObjectEnabled = false;
+ }
+
+ public void FinishEditSprite(bool commit)
+ {
+ if (!EditSelectedSpriteEnabled || SelectedSpriteIndex == -1 || OldSpriteIndex == -1)
+ {
+ return;
+ }
+
+ if (!commit)
+ {
+ SelectedSpriteIndex = EditSpriteInternal(
+ SelectedSpriteIndex,
+ OldSprite,
+ discardHistory: true);
+ }
+ else if (OldSprite != SelectedSprite)
+ {
+ PushSpriteEdited(
+ oldIndex: OldSpriteIndex,
+ oldItem: OldSprite,
+ newIndex: SelectedSpriteIndex,
+ newItem: SelectedSprite);
+ }
+
+ OldSpriteIndex = -1;
+ EditSelectedSpriteEnabled = false;
+ }
+
+ public void SetSelectedItem(int x, int y)
+ {
+
+ if (SpriteMode)
+ {
+ SelectedSpriteIndex = SpriteData.AtCoords(x, y);
+ if (SelectedSpriteIndex == -1)
+ {
+ PreferredX = x;
+ PreferredY = y;
+ }
+ }
+ else
+ {
+ SelectedObjectIndex = ObjectData.AtCoords(x, y - 2);
+ if (SelectedObjectIndex == -1)
+ {
+ PreferredX = x;
+ PreferredY = y - 2;
+ }
+ }
+ }
+
+ public void MoveSelectedItem(int x, int y)
+ {
+ if (SpriteMode)
+ {
+ if (!EditSelectedSpriteEnabled)
+ {
+ return;
+ }
+ }
+ else if (!EditSelectedObjectEnabled)
+ {
+ return;
+ }
+
+ if (x < 0)
+ {
+ x = 0;
+ }
+ else if (x >= 0x200)
+ {
+ x = 0x1FF;
+ }
+
+ if (SpriteMode && SelectedSpriteIndex != -1)
+ {
+ if (y < 0)
+ {
+ y = 0;
+ }
+ else if (y >= 0x0D)
+ {
+ y = 0x0C;
+ }
+
+ var item = SelectedSprite;
+ item.X = x;
+ item.Y = y;
+
+ SelectedSpriteIndex = EditSpriteInternal(
+ SelectedSpriteIndex,
+ item,
+ discardHistory: true);
+ }
+ else if (!SpriteMode && SelectedObjectIndex != -1)
+ {
+ if (y - 2 < 0)
+ {
+ y = 2;
+ }
+ else if (y - 2 >= 0x0C)
+ {
+ y = 2 + 0x0B;
+ }
+
+ var item = SelectedObject;
+ item.X = x;
+ item.Y = y - 2;
+
+ SelectedObjectIndex = EditObjectInternal(
+ SelectedObjectIndex,
+ item,
+ discardHistory: true);
+ }
+ }
+
+ public DrawData GetDrawData(
+ int startX,
+ Size size,
+ Color separatorColor,
+ Color passiveColor,
+ Color selectColor)
+ {
+ var rectangles = (SpriteMode ? GetSpriteRectangles() : GetObjectRectangles())
+ .ToArray();
+ var selectedIndex = SpriteMode
+ ? SelectedSpriteIndex
+ : SelectedObjectIndex;
+
+ return new DrawData(
+ new Color32BppArgb(0xFF, 0, 0, 0),
+ Palette,
+ PixelData,
+ BG1,
+ EnumerateSprites(AnimationFrame),
+ startX,
+ size,
+ rectangles,
+ selectedIndex,
+ separatorColor,
+ passiveColor,
+ selectColor);
+ }
+
+ protected virtual void OnFileOpened(EventArgs e)
+ {
+ LastAutoSaveTime = DateTime.Now.ToUniversalTime();
+ LastAutoSaveData = Rom!.GetData();
+ FileOpened?.Invoke(this, e);
+ }
+
+ protected virtual void OnFileSaved(EventArgs e)
+ {
+ SaveHistoryIndex = CurrentHistoryIndex;
+ LastAutoSaveTime = DateTime.Now.ToUniversalTime();
+ LastAutoSaveData = Rom!.GetData();
+ FileSaved?.Invoke(this, e);
+ }
+
+ protected virtual void OnAreaLoaded(EventArgs e)
+ {
+ AreaLoaded?.Invoke(this, e);
+ }
+
+ protected virtual void OnHasUnsavedChangesChanged(EventArgs e)
+ {
+ HasUnsavedChangesChanged?.Invoke(this, e);
+ }
+
+ protected virtual void UndoFactory_UndoComplete(object? sender, UndoEventArgs e)
+ {
+ CurrentHistoryIndex--;
+ UndoComplete?.Invoke(this, e);
+ }
+
+ protected virtual void UndoFactory_RedoComplete(object? sender, UndoEventArgs e)
+ {
+ CurrentHistoryIndex++;
+ RedoComplete?.Invoke(this, e);
+ }
+
+ private void UndoFactory_Cleared(object? sender, EventArgs e)
+ {
+ HistoryCleared?.Invoke(this, EventArgs.Empty);
+ }
+
+ protected virtual void UndoFactory_UndoElementAdded(object? sender, UndoEventArgs e)
+ {
+ UndoElementAdded?.Invoke(this, EventArgs.Empty);
+ }
+
+ protected virtual void OnItemCopied(EventArgs e)
+ {
+ ItemCopied?.Invoke(this, e);
+ }
+
+ protected virtual void OnPathChanged(EventArgs e)
+ {
+ var dir = System.IO.Path.GetDirectoryName(Path) ?? String.Empty;
+ var name = System.IO.Path.GetFileNameWithoutExtension(Path);
+ AutoSavePath = System.IO.Path.Combine(
+ dir,
+ name + "_AutoSaves",
+ System.IO.Path.GetFileName(Path));
+ PathChanged?.Invoke(this, e);
+ }
+
+ protected virtual void OnCopiedSpriteChanged(EventArgs e)
+ {
+ CopiedSpriteChanged?.Invoke(this, e);
+ OnItemCopied(EventArgs.Empty);
+ }
+
+ protected virtual void OnCopiedObjectChanged(EventArgs e)
+ {
+ CopiedObjectChanged?.Invoke(this, e);
+ OnItemCopied(EventArgs.Empty);
+ }
+
+ protected virtual void OnAreaNumberChanged(EventArgs e)
+ {
+ AreaNumberChanged?.Invoke(this, e);
+ if (IsOpen)
+ {
+ ReloadAreaInternal();
+ ClearHistory();
+ }
+ }
+
+ private void ClearHistory()
+ {
+ UndoFactory.Clear();
+ _saveHistoryIndex = 0;
+ _currentHistoryIndex = 0;
+ HasUnsavedChanges = false;
+ }
+
+ protected virtual void OnAreaHeaderChanged(EventArgs e)
+ {
+ AreaHeaderChanged?.Invoke(this, e);
+ if (IsAreaLoaded)
+ {
+ RenderAreaTilemap();
+ Invalidate();
+ }
+ }
+
+ protected virtual void OnSpriteModeChanged(EventArgs e)
+ {
+ SpriteModeChanged?.Invoke(this, e);
+
+ if (IsAreaLoaded)
+ {
+ Invalidate();
+ }
+ }
+
+ protected virtual void OnStartXChanged(EventArgs e)
+ {
+ StartXChanged?.Invoke(this, e);
+
+ Invalidated?.Invoke(this, EventArgs.Empty);
+ }
+
+ protected virtual void OnPlayerChanged(EventArgs e)
+ {
+ PlayerChanged?.Invoke(this, e);
+ if (IsAreaLoaded)
+ {
+ ReloadPalette();
+ Invalidate();
+ }
+ }
+
+ protected virtual void OnPlayerStateChanged(EventArgs e)
+ {
+ PlayerStateChanged?.Invoke(this, e);
+ if (IsAreaLoaded)
+ {
+ ReloadPalette();
+ Invalidate();
+ }
+ }
+
+ protected virtual void OnSelectedObjectChanged(EventArgs e)
+ {
+ SelectedObjectChanged?.Invoke(this, e);
+ if (!SpriteMode && IsAreaLoaded)
+ {
+ Invalidate();
+ }
+ }
+
+ protected virtual void OnObjectData_DataReset(EventArgs e)
+ {
+ ObjectData_DataReset?.Invoke(this, e);
+ SelectedObjectIndex = ObjectData.Count > 0 ? 0 : -1;
+ OnSelectedObjectChanged(EventArgs.Empty);
+ if (IsAreaLoaded)
+ {
+ RenderAreaTilemap();
+ Invalidate();
+ }
+ }
+
+ protected virtual void OnObjectData_ItemEdited(ObjectEditedEventArgs e)
+ {
+ ObjectData_ItemEdited?.Invoke(this, e);
+ if (IsAreaLoaded)
+ {
+ RenderAreaTilemap();
+ Invalidate();
+ }
+ }
+
+ protected virtual void OnObjectData_ItemAdded(ItemAddedEventArgs e)
+ {
+ ObjectData_ItemAdded?.Invoke(this, e);
+ if (IsAreaLoaded)
+ {
+ RenderAreaTilemap();
+ Invalidate();
+ }
+ }
+
+ protected virtual void OnObjectData_ItemRemoved(ItemAddedEventArgs e)
+ {
+ ObjectData_ItemRemoved?.Invoke(this, e);
+ if (SelectedObjectIndex >= ObjectData.Count)
+ {
+ SelectedObjectIndex = ObjectData.Count - 1;
+ }
+
+ RenderAreaTilemap();
+ Invalidate();
+ }
+
+ protected virtual void OnObjectData_DataCleared(EventArgs e)
+ {
+ ObjectData_DataCleared?.Invoke(this, e);
+ RenderAreaTilemap();
+ Invalidate();
+ }
+
+ protected virtual void OnSelectedSpriteChanged(EventArgs e)
+ {
+ SelectedSpriteChanged?.Invoke(this, e);
+ if (SpriteMode)
+ {
+ Invalidate();
+ }
+ }
+
+ protected virtual void OnAnimationFrameChanged(EventArgs e)
+ {
+ GameData?.GfxData.ReadAnimationFrame(AnimationFrame, PixelData);
+
+ AnimationFrameChanged?.Invoke(this, e);
+ Invalidate();
+ }
+
+ protected virtual void OnInvalidated(EventArgs e)
+ {
+ if (IsAreaLoaded)
+ {
+ Invalidated?.Invoke(this, e);
+ }
+ }
+
+ private static int PlayerPose(StartYPosition position)
+ {
+ return position switch
+ {
+ StartYPosition.Y00 => 4,
+ StartYPosition.Y20 => 4,
+ StartYPosition.YB0 => 2,
+ StartYPosition.Y50 => 2,
+ StartYPosition.Alt1Y00 => 4,
+ StartYPosition.Alt2Y00 => 4,
+ StartYPosition.PipeIntroYB0 => 0,
+ StartYPosition.AltPipeIntroYB0 => 0,
+ _ => 6,
+ };
+ }
+
+ private void WriteToGameData()
+ {
+ WriteHeader();
+ WriteObjectData();
+ WriteSpriteData();
+ GameData!.WriteToGameData(Rom!);
+ }
+
+ private void SetAreaNumberInternal(
+ int oldAreaNumber,
+ int newAreaNumber,
+ bool discardHistory = false)
+ {
+ _areaNumber = newAreaNumber;
+ OnAreaNumberChanged(EventArgs.Empty);
+ }
+
+ private int AddObjectInternal(
+ UIAreaObjectCommand item,
+ bool discardHistory = false)
+ {
+ var index = ObjectData.Add(item);
+ if (!discardHistory)
+ {
+ PushObjectAdded(index);
+ }
+
+ return index;
+ }
+
+ private int EditObjectInternal(
+ int index,
+ UIAreaObjectCommand newItem,
+ bool discardHistory = false)
+ {
+ var oldItem = ObjectData[index];
+ var newIndex = ObjectData.Edit(index, newItem);
+ if (!discardHistory)
+ {
+ PushObjectEdited(
+ oldIndex: index,
+ oldItem: oldItem,
+ newIndex: newIndex,
+ newItem: newItem);
+ }
+
+ return newIndex;
+ }
+
+ private void PushObjectAdded(int index)
+ {
+ var item = ObjectData[index];
+ PushUndoAction(
+ undo: () => ObjectData.RemoveAt(index),
+ redo: () => ObjectData.Add(item));
+ }
+
+ private int AddSpriteInternal(
+ UIAreaSpriteCommand item,
+ bool discardHistory = false)
+ {
+ var index = SpriteData.Add(item);
+ if (!discardHistory)
+ {
+ PushSpriteAdded(index);
+ }
+
+ return index;
+ }
+
+ private void PushSpriteAdded(int index)
+ {
+ var item = SpriteData[index];
+ PushUndoAction(
+ undo: () => SpriteData.RemoveAt(index),
+ redo: () => SpriteData.Add(item));
+ }
+
+ private void PushObjectEdited(
+ int oldIndex,
+ UIAreaObjectCommand oldItem,
+ int newIndex,
+ UIAreaObjectCommand newItem)
+ {
+ PushUndoAction(undo, redo);
+
+ void undo()
+ {
+ var index = ObjectData.Edit(newIndex, oldItem);
+ Debug.Assert(index == oldIndex);
+
+ if (SelectedObjectIndex == newIndex)
+ {
+ SelectedObjectIndex = oldIndex;
+ }
+ else if (IsAscending(newIndex, SelectedObjectIndex, oldIndex))
+ {
+ SelectedObjectIndex--;
+ }
+ else if (IsAscending(oldIndex, SelectedObjectIndex, newIndex))
+ {
+ SelectedObjectIndex++;
+ }
+ }
+
+ void redo()
+ {
+ var index = ObjectData.Edit(oldIndex, newItem);
+ Debug.Assert(index == newIndex);
+
+ if (SelectedObjectIndex == oldIndex)
+ {
+ SelectedObjectIndex = index;
+ }
+ else if (IsAscending(oldIndex, SelectedObjectIndex, newIndex))
+ {
+ SelectedObjectIndex--;
+ }
+ else if (IsAscending(newIndex, SelectedObjectIndex, oldIndex))
+ {
+ SelectedObjectIndex++;
+ }
+ }
+ }
+
+ private static bool IsAscending(params T[] values)
+ {
+ return IsAscending(Comparer.Default, values);
+ }
+
+ private static bool IsAscending(IComparer? comparer, params T[] values)
+ {
+ comparer ??= Comparer.Default;
+
+ if (values.Length < 2)
+ {
+ return true;
+ }
+
+ for (var i = 1; i < values.Length; i++)
+ {
+ if (comparer.Compare(values[i - 1], values[i]) > 0)
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ private int EditSpriteInternal(
+ int index,
+ UIAreaSpriteCommand newItem,
+ bool discardHistory = false)
+ {
+ var oldItem = SpriteData[index];
+ var newIndex = SpriteData.Edit(index, newItem);
+ if (!discardHistory)
+ {
+ PushSpriteEdited(
+ oldIndex: index,
+ oldItem: oldItem,
+ newIndex: newIndex,
+ newItem: newItem);
+ }
+
+ return newIndex;
+ }
+
+ private void PushSpriteEdited(
+ int oldIndex,
+ UIAreaSpriteCommand oldItem,
+ int newIndex,
+ UIAreaSpriteCommand newItem)
+ {
+ PushUndoAction(undo, redo);
+
+ void undo()
+ {
+ var index = SpriteData.Edit(newIndex, oldItem);
+ Debug.Assert(index == oldIndex);
+
+ if (SelectedSpriteIndex == newIndex)
+ {
+ SelectedSpriteIndex = oldIndex;
+ }
+ else if (IsAscending(newIndex, SelectedSpriteIndex, oldIndex))
+ {
+ SelectedSpriteIndex--;
+ }
+ else if (IsAscending(oldIndex, SelectedSpriteIndex, newIndex))
+ {
+ SelectedSpriteIndex++;
+ }
+ }
+
+ void redo()
+ {
+ var index = SpriteData.Edit(oldIndex, newItem);
+ Debug.Assert(index == newIndex);
+
+ if (SelectedSpriteIndex == oldIndex)
+ {
+ SelectedSpriteIndex = index;
+ }
+ else if (IsAscending(oldIndex, SelectedSpriteIndex, newIndex))
+ {
+ SelectedSpriteIndex--;
+ }
+ else if (IsAscending(newIndex, SelectedSpriteIndex, oldIndex))
+ {
+ SelectedSpriteIndex++;
+ }
+ }
+ }
+
+ private void RemoveObjectInternal(
+ int index,
+ bool discardHistory = false)
+ {
+ var oldItem = ObjectData[index];
+ ObjectData.RemoveAt(index);
+ if (!discardHistory)
+ {
+ PushObjectRemoved(index, oldItem);
+ }
+ }
+
+ private void PushObjectRemoved(int index, UIAreaObjectCommand item)
+ {
+ PushUndoAction(
+ undo: () => ObjectData.Add(item),
+ redo: () => ObjectData.RemoveAt(index));
+
+ }
+
+ private void RemoveSpriteInternal(
+ int index,
+ bool discardHistory = false)
+ {
+ var oldItem = SpriteData[index];
+ SpriteData.RemoveAt(index);
+ if (!discardHistory)
+ {
+ PushSpriteRemoved(index, oldItem);
+ }
+ }
+
+ private void PushSpriteRemoved(int index, UIAreaSpriteCommand item)
+ {
+ PushUndoAction(
+ undo: () => SpriteData.Add(item),
+ redo: () => SpriteData.RemoveAt(index));
+ }
+
+ private void ClearObjectsInternal(bool discardHistory = false)
+ {
+ var items = ObjectData.ToArray();
+ ObjectData.Clear();
+ SelectedObjectIndex = -1;
+ if (!discardHistory)
+ {
+ PushObjectsCleared(items);
+ }
+ }
+
+ private void PushObjectsCleared(UIAreaObjectCommand[] items)
+ {
+ PushUndoAction(
+ undo: () => ObjectData.Reset(items),
+ redo: () => ObjectData.Clear());
+ }
+
+ private void ClearSpritesInternal(bool discardHistory = false)
+ {
+ var items = SpriteData.ToArray();
+ SpriteData.Clear();
+ SelectedSpriteIndex = -1;
+ if (!discardHistory)
+ {
+ PushSpritesCleared(items);
+ }
+ }
+
+ private void PushSpritesCleared(UIAreaSpriteCommand[] items)
+ {
+ PushUndoAction(
+ undo: () => SpriteData.Reset(items),
+ redo: () => SpriteData.Clear());
+ }
+
+ private void SetAreaHeaderInternal(
+ AreaHeader oldAreaHeader,
+ AreaHeader newAreaHeader,
+ bool discardHistory = false)
+ {
+ _areaHeader = newAreaHeader;
+ OnAreaHeaderChanged(EventArgs.Empty);
+ if (!discardHistory)
+ {
+ PushAreaHeaderChanged(oldAreaHeader, newAreaHeader);
+ }
+ }
+
+ private void SetObjectInternal(
+ int index,
+ UIAreaObjectCommand newItem,
+ bool discardHistory = false)
+ {
+ var oldItem = ObjectData[index];
+ _selectedObjectIndex = ObjectData.Edit(index, newItem);
+ if (!discardHistory)
+ {
+ PushObjectEdited(
+ oldIndex: index,
+ oldItem: oldItem,
+ newIndex: SelectedObjectIndex,
+ newItem: newItem);
+ }
+ }
+
+ private void PushAreaHeaderChanged(
+ AreaHeader oldAreaHeader,
+ AreaHeader newAreaHeader)
+ {
+ PushUndoAction(
+ undo: () => SetAreaHeaderInternal(
+ oldAreaHeader: newAreaHeader,
+ newAreaHeader: oldAreaHeader,
+ discardHistory: true),
+ redo: () => SetAreaHeaderInternal(
+ oldAreaHeader: oldAreaHeader,
+ newAreaHeader: newAreaHeader,
+ discardHistory: true));
+ }
+
+ private void PushUndoAction(Action undo, Action redo)
+ {
+ CurrentHistoryIndex++;
+ UndoFactory.Add(undo, redo);
+ }
+
+ private void ReloadAreaInternal()
+ {
+ IsAreaLoaded = false;
+ SetAreaHeaderInternal(
+ AreaHeader,
+ GameData!.AreaLoader.Headers[ObjectAreaIndex],
+ discardHistory: true);
+ ResetObjectData(discardHistory: true);
+ ResetSpriteData(discardHistory: true);
+ ReloadPalette();
+ GameData!.TilemapLoader.LoadTilemap(ObjectAreaIndex);
+ ReloadGfx();
+ StartX = 0;
+ IsAreaLoaded = true;
+ RenderAreaTilemap();
+ OnAreaLoaded(EventArgs.Empty);
+ Invalidate();
+ }
+
+ private void ReloadPalette()
+ {
+ // TODO(nrg): Change AreaNumber requirement to tileset requirement.
+ var isLuigiBonusArea = Player == Player.Luigi
+ && (AreaNumber == 0x42 || AreaNumber == 0x2B);
+ GameData!.PaletteData.ReadPalette(
+ ObjectAreaIndex,
+ isLuigiBonusArea,
+ Player,
+ PlayerState,
+ Palette);
+ }
+
+ private void ReloadGfx()
+ {
+ GameData!.GfxData.ReadAreaTileSet(
+ ObjectAreaIndex,
+ GameData.TilemapLoader.TileSetIndex,
+ Player,
+ PixelData);
+ }
+
+ private void ResetObjectData(bool discardHistory = false)
+ {
+ if (!discardHistory)
+ {
+ throw new NotImplementedException();
+ }
+
+ ObjectData.Reset(GameData!.AreaLoader.AreaObjectData[ObjectAreaIndex]);
+ }
+
+ private void WriteObjectData()
+ {
+ GameData!.AreaLoader.AreaObjectData[ObjectAreaIndex] = ObjectData.GetObjectData().ToArray();
+ }
+
+ private void WriteSpriteData()
+ {
+ GameData!.AreaLoader.AreaSpriteData[SpriteAreaIndex] = SpriteData.GetSpriteData().ToArray();
+ }
+
+ private void WriteHeader()
+ {
+ GameData!.AreaLoader.Headers[ObjectAreaIndex] = AreaHeader;
+ }
+
+ private void ResetSpriteData(bool discardHistory = false)
+ {
+ if (!discardHistory)
+ {
+ throw new NotImplementedException();
+ }
+
+ SpriteData.Reset(GameData!.AreaLoader.AreaSpriteData[SpriteAreaIndex]);
+ }
+
+ private void RenderAreaTilemap()
+ {
+ GameData!.AreaObjectRenderer.RenderTileMap(
+ TileMap,
+ AreaType,
+ AreaHeader,
+ ObjectData.GetObjectData().ToArray(),
+ //GameData!.AreaLoader.AreaObjectData[ObjectAreaIndex],
+ AreaNumber == 2);
+ ReadBG1Tiles();
+ }
+
+ private void ReadBG1Tiles()
+ {
+ const int width = 0x200;
+ var tiles = TileMap;
+ var height = tiles.Length / width;
+ for (var y = 0; y < height; y++)
+ {
+ var srcRow = y * width;
+ var destRow = srcRow << 2;
+ for (var srcX = 0; srcX < width; srcX++)
+ {
+ var destX = srcX << 1;
+ var index = srcRow + srcX;
+ var tileIndex = (byte)tiles[index];
+ var tile = Map16Tiles[tileIndex];
+ if (tileIndex is 0x56 or 0x57)
+ {
+ if (srcX > 0 && ((byte)tiles[index - 1]) == 0)
+ {
+ tile.TopLeft += 4;
+ tile.BottomLeft += 4;
+ }
+
+ if (srcX + 1 < width && ((byte)tiles[index + 1]) == 0)
+ {
+ tile.TopRight += 4;
+ tile.BottomRight += 4;
+ }
+ }
+
+ BG1[destRow + destX] = tile.TopLeft;
+ BG1[destRow + destX + 1] = tile.TopRight;
+ BG1[destRow + (width << 1) + destX] = tile.BottomLeft;
+ BG1[destRow + (width << 1) + destX + 1] = tile.BottomRight;
+ }
+ }
+ }
+
+ private IEnumerable GetObjectRectangles()
+ {
+ return ObjectData.Select(p => p.SelectionRectangle);
+ }
+
+ private IEnumerable GetSpriteRectangles()
+ {
+ return SpriteData.Select(p => p.SelectionRectangle);
+ }
+
+ private IEnumerable EnumerateSprites(int frame)
+ {
+ var areaDataSprites = GameData!.AreaSpriteRenderer.GetSprites(
+ SpriteData.GetSpriteData().ToArray(),
+ ObjectData.GetObjectData().ToArray(),
+ frame,
+ AreaType,
+ showPipePiranhaPlants: AreaNumber != 0x25);
+
+ var tilemapSprites = AreaSpriteRenderer.GetObjectDataSprites(TileMap);
+
+ var playerSprite = AreaSpriteRenderer.GetPlayerSprite(
+ x: 0x28,
+ AreaHeader.StartYPixel,
+ Player,
+ PlayerState,
+ PlayerPose(AreaHeader.StartYPosition));
+
+ return areaDataSprites.Concat(tilemapSprites).Concat(playerSprite);
+ }
+
+ private void Invalidate()
+ {
+ OnInvalidated(EventArgs.Empty);
+ }
+}
diff --git a/src/Brutario.Core/DrawData.cs b/src/Brutario.Core/DrawData.cs
new file mode 100644
index 0000000..480b9c2
--- /dev/null
+++ b/src/Brutario.Core/DrawData.cs
@@ -0,0 +1,62 @@
+namespace Brutario.Core;
+
+using System;
+using System.Collections.Generic;
+using System.Drawing;
+
+using Maseya.Snes;
+
+public readonly ref struct DrawData
+{
+ public DrawData(
+ Color32BppArgb bgColor,
+ ReadOnlySpan palette,
+ ReadOnlySpan pixelData,
+ ReadOnlySpan bg1,
+ IEnumerable sprites,
+ int startX,
+ Size size,
+ IEnumerable rectangles,
+ int selectedIndex,
+ Color separatorColor,
+ Color passiveColor,
+ Color selectColor)
+ {
+ BgColor = bgColor;
+ Palette = palette;
+ PixelData = pixelData;
+ Bg1 = bg1;
+ Sprites = sprites;
+ StartX = startX;
+ Size = size;
+ Rectangles = rectangles;
+ SelectedIndex = selectedIndex;
+ SeparatorColor = separatorColor;
+ PassiveColor = passiveColor;
+ SelectColor = selectColor;
+ }
+
+ public Color32BppArgb BgColor { get; }
+
+ public ReadOnlySpan Palette { get; }
+
+ public ReadOnlySpan PixelData { get; }
+
+ public ReadOnlySpan Bg1 { get; }
+
+ public IEnumerable Sprites { get; }
+
+ public int StartX { get; }
+
+ public Size Size { get; }
+
+ public IEnumerable Rectangles { get; }
+
+ public int SelectedIndex { get; }
+
+ public Color SeparatorColor { get; }
+
+ public Color PassiveColor { get; }
+
+ public Color SelectColor { get; }
+}
diff --git a/src/Brutario.Core/Editors/IFileNameSelector.cs b/src/Brutario.Core/Editors/IFileNameSelector.cs
new file mode 100644
index 0000000..743657a
--- /dev/null
+++ b/src/Brutario.Core/Editors/IFileNameSelector.cs
@@ -0,0 +1,14 @@
+//
+// Copyright (c) 2022 spel werdz rite. All rights reserved. Licensed under GNU Affero
+// General Public License. See LICENSE in project root for full license
+// information, or visit https://www.gnu.org/licenses/#AGPL
+//
+
+namespace Brutario.Core;
+
+public interface IFileNameSelector
+{
+ string? FileName { get; set; }
+
+ PromptResult Prompt();
+}
diff --git a/src/Brutario.Core/Editors/IHeaderEditorView.cs b/src/Brutario.Core/Editors/IHeaderEditorView.cs
new file mode 100644
index 0000000..fd15e60
--- /dev/null
+++ b/src/Brutario.Core/Editors/IHeaderEditorView.cs
@@ -0,0 +1,20 @@
+//
+// Copyright (c) 2022 spel werdz rite. All rights reserved. Licensed under GNU Affero
+// General Public License. See LICENSE in project root for full license
+// information, or visit https://www.gnu.org/licenses/#AGPL
+//
+
+namespace Brutario.Core;
+
+using System;
+
+using Maseya.Smas.Smb1.AreaData.HeaderData;
+
+public interface IHeaderEditorView
+{
+ event EventHandler? AreaHeaderChanged;
+
+ AreaHeader AreaHeader { get; set; }
+
+ bool Prompt();
+}
diff --git a/src/Brutario.Core/Editors/IMainEditor.cs b/src/Brutario.Core/Editors/IMainEditor.cs
new file mode 100644
index 0000000..b26d1ec
--- /dev/null
+++ b/src/Brutario.Core/Editors/IMainEditor.cs
@@ -0,0 +1,114 @@
+//
+// Copyright (c) 2022 spel werdz rite. All rights reserved. Licensed under GNU Affero
+// General Public License. See LICENSE in project root for full license
+// information, or visit https://www.gnu.org/licenses/#AGPL
+//
+
+namespace Brutario.Core;
+
+using System;
+using System.Collections.Generic;
+
+using Maseya.Smas.Smb1;
+using Maseya.Smas.Smb1.AreaData.HeaderData;
+
+public interface IMainEditor
+{
+ event EventHandler? PathChanged;
+
+ event EventHandler? FileOpened;
+
+ event EventHandler? FileSaved;
+
+ event EventHandler? FileClosed;
+
+ event EventHandler? UndoElementAdded;
+
+ event EventHandler? UndoComplete;
+
+ event EventHandler? RedoComplete;
+
+ event EventHandler? PlayerChanged;
+
+ event EventHandler? PlayerStateChanged;
+
+ event EventHandler? SpriteModeChanged;
+
+ event EventHandler? AreaNumberChanged;
+
+ event EventHandler? AreaHeaderChanged;
+
+ event EventHandler? StartXChanged;
+
+ event EventHandler? SelectedObjectChanged;
+
+ event EventHandler? SelectedSpriteChanged;
+
+ event EventHandler? Invalidated;
+
+ event EventHandler? ObjectData_DataReset;
+
+ event EventHandler? ObjectData_ItemEdited;
+
+ event EventHandler? ObjectData_ItemAdded;
+
+ event EventHandler? ObjectData_ItemRemoved;
+
+ event EventHandler? ObjectData_DataCleared;
+
+ string Path { get; }
+
+ bool IsOpen { get; }
+
+ bool HasUnsavedChanges { get; }
+
+ bool CanUndo { get; }
+
+ bool CanRedo { get; }
+
+ /*
+ int AnimationFrame { get; }
+ */
+
+ Player Player { get; set; }
+
+ PlayerState PlayerState { get; set; }
+
+ bool SpriteMode { get; set; }
+
+ int AreaNumber { get; set; }
+
+ int StartX { get; set; }
+
+ AreaHeader AreaHeader { get; set; }
+
+ IReadOnlyList ObjectData { get; }
+
+ IReadOnlyList SpriteData { get; }
+
+ int SelectedObjectIndex { get; set; }
+
+ int SelectedSpriteIndex { get; set; }
+
+ /*
+ event EventHandler AnimationFrameChanged;
+ */
+
+ void Open(string path);
+
+ void Save();
+
+ void SaveAs(string path);
+
+ void Close();
+
+ void Undo();
+
+ void Redo();
+
+ void SetSelectedItem(int x, int y);
+
+ void InitializeSetAreaHeader(AreaHeader newAreaHeader);
+
+ void FinishSetAreaHeader(AreaHeader oldAreaHeader, bool commit);
+}
diff --git a/src/Brutario.Core/Editors/IObjectEditorView.cs b/src/Brutario.Core/Editors/IObjectEditorView.cs
new file mode 100644
index 0000000..9480969
--- /dev/null
+++ b/src/Brutario.Core/Editors/IObjectEditorView.cs
@@ -0,0 +1,24 @@
+//
+// Copyright (c) 2022 spel werdz rite. All rights reserved. Licensed under GNU Affero
+// General Public License. See LICENSE in project root for full license
+// information, or visit https://www.gnu.org/licenses/#AGPL
+//
+
+namespace Brutario.Core;
+
+using System;
+
+using Maseya.Smas.Smb1.AreaData.ObjectData;
+
+public interface IObjectEditorView
+{
+ event EventHandler? AreaPlatformTypeChanged;
+
+ event EventHandler? AreaObjectCommandChanged;
+
+ AreaPlatformType AreaPlatformType { get; set; }
+
+ UIAreaObjectCommand AreaObjectCommand { get; set; }
+
+ bool PromptConfirm();
+}
diff --git a/src/Brutario.Core/Editors/ISaveOnClosePrompt.cs b/src/Brutario.Core/Editors/ISaveOnClosePrompt.cs
new file mode 100644
index 0000000..c2bbe99
--- /dev/null
+++ b/src/Brutario.Core/Editors/ISaveOnClosePrompt.cs
@@ -0,0 +1,12 @@
+//
+// Copyright (c) 2022 spel werdz rite. All rights reserved. Licensed under GNU Affero
+// General Public License. See LICENSE in project root for full license
+// information, or visit https://www.gnu.org/licenses/#AGPL
+//
+
+namespace Brutario.Core;
+
+public interface ISaveOnClosePrompt
+{
+ PromptResult Prompt();
+}
diff --git a/src/Brutario.Core/Editors/ISpriteEditorView.cs b/src/Brutario.Core/Editors/ISpriteEditorView.cs
new file mode 100644
index 0000000..f75d9c5
--- /dev/null
+++ b/src/Brutario.Core/Editors/ISpriteEditorView.cs
@@ -0,0 +1,22 @@
+//
+// Copyright (c) 2022 spel werdz rite. All rights reserved. Licensed under GNU Affero
+// General Public License. See LICENSE in project root for full license
+// information, or visit https://www.gnu.org/licenses/#AGPL
+//
+
+namespace Brutario.Core;
+
+using System;
+
+public interface ISpriteEditorView
+{
+ event EventHandler? AreaSpriteCommandChanged;
+
+ UIAreaSpriteCommand AreaSpriteCommand
+ {
+ get;
+ set;
+ }
+
+ bool PromptConfirm();
+}
diff --git a/src/Brutario.Core/Editors/PromptResult.cs b/src/Brutario.Core/Editors/PromptResult.cs
new file mode 100644
index 0000000..421fa1b
--- /dev/null
+++ b/src/Brutario.Core/Editors/PromptResult.cs
@@ -0,0 +1,14 @@
+//
+// Copyright (c) 2022 spel werdz rite. All rights reserved. Licensed under GNU Affero
+// General Public License. See LICENSE in project root for full license
+// information, or visit https://www.gnu.org/licenses/#AGPL
+//
+
+namespace Brutario.Core;
+
+public enum PromptResult
+{
+ Yes,
+ No,
+ Cancel
+}
diff --git a/src/Brutario.Core/ItemAddedEventArgs.cs b/src/Brutario.Core/ItemAddedEventArgs.cs
new file mode 100644
index 0000000..9d369cf
--- /dev/null
+++ b/src/Brutario.Core/ItemAddedEventArgs.cs
@@ -0,0 +1,12 @@
+namespace Brutario.Core;
+using System;
+
+public class ItemAddedEventArgs : EventArgs
+{
+ public ItemAddedEventArgs(int index)
+ {
+ Index = index;
+ }
+
+ public int Index { get; set; }
+}
diff --git a/src/Brutario.Core/ObjectEditedEventArgs.cs b/src/Brutario.Core/ObjectEditedEventArgs.cs
new file mode 100644
index 0000000..0c5cabd
--- /dev/null
+++ b/src/Brutario.Core/ObjectEditedEventArgs.cs
@@ -0,0 +1,25 @@
+namespace Brutario.Core;
+using System;
+
+public class ObjectEditedEventArgs : EventArgs
+{
+ public ObjectEditedEventArgs(
+ int oldIndex,
+ int newIndex,
+ UIAreaObjectCommand oldCommand,
+ UIAreaObjectCommand newCommand)
+ {
+ OldIndex = oldIndex;
+ NewIndex = newIndex;
+ OldCommand = oldCommand;
+ NewCommand = newCommand;
+ }
+
+ public int OldIndex { get; set; }
+
+ public int NewIndex { get; set; }
+
+ public UIAreaObjectCommand OldCommand { get; set; }
+
+ public UIAreaObjectCommand NewCommand { get; set; }
+}
diff --git a/src/Brutario.Core/Presenters/MainPresenter.cs b/src/Brutario.Core/Presenters/MainPresenter.cs
new file mode 100644
index 0000000..fc0d927
--- /dev/null
+++ b/src/Brutario.Core/Presenters/MainPresenter.cs
@@ -0,0 +1,770 @@
+//
+// Copyright (c) 2022 spel werdz rite. All rights reserved. Licensed under GNU Affero
+// General Public License. See LICENSE in project root for full license
+// information, or visit https://www.gnu.org/licenses/#AGPL
+//
+
+namespace Brutario.Core;
+
+using System;
+using System.Drawing;
+using System.Text;
+
+using Brutario.Core.Views;
+
+using Maseya.Smas.Smb1;
+
+public class MainPresenter
+{
+ public MainPresenter(
+ BrutarioEditor mainEditor,
+ IMainView mainView,
+ IObjectListView objectListView,
+ IExceptionView exceptionHelper,
+ IFileNameSelector openFileNameSelector,
+ IFileNameSelector saveFileNameSelector,
+ ISaveOnClosePrompt saveOnClosePrompt,
+ IHeaderEditorView headerEditor,
+ IObjectEditorView objectEditor,
+ ISpriteEditorView spriteEditor)
+ {
+ MainEditor = mainEditor;
+ MainView = mainView;
+ //ObjectListView = objectListView;
+ ExceptionHelper = exceptionHelper;
+ OpenFileNameSelector = openFileNameSelector;
+ SaveFileNameSelector = saveFileNameSelector;
+ SaveOnClosePrompt = saveOnClosePrompt;
+ HeaderEditorView = headerEditor;
+ ObjectEditorView = objectEditor;
+ SpriteEditorView = spriteEditor;
+
+ MainEditor.PathChanged += MainEditor_PathChanged;
+ MainEditor.FileOpened += MainEditor_FileOpened;
+ MainEditor.FileSaved += MainEditor_FileSaved;
+ MainEditor.FileClosed += MainEditor_FileClosed;
+ MainEditor.HasUnsavedChangesChanged += MainEditor_HasUnsavedChangesChanged;
+ MainEditor.UndoComplete += MainEditor_UndoComplete;
+ MainEditor.RedoComplete += MainEditor_RedoComplete;
+ MainEditor.ItemCopied += MainEditor_ItemCopied;
+ MainEditor.SpriteModeChanged += MainEditor_SpriteModeChanged;
+ MainEditor.PlayerChanged += MainEditor_PlayerChanged;
+ MainEditor.PlayerStateChanged += MainEditor_PlayerStateChanged;
+ MainEditor.Invalidated += MainEditor_Invalidated;
+ MainEditor.AreaNumberChanged += MainEditor_AreaNumberChanged;
+ MainEditor.AreaLoaded += MainEditor_AreaLoaded;
+ MainEditor.StartXChanged += MainEditor_StartXChanged;
+ MainEditor.HistoryCleared += MainEditor_HistoryCleared;
+ MainEditor.UndoElementAdded += MainEditor_UndoElementAdded;
+ MainEditor.AreaHeaderChanged += MainEditor_AreaHeaderChanged;
+ MainEditor.SelectedObjectChanged +=
+ MainEditor_ObjectDataSelectedIndexChanged;
+ MainEditor.SelectedSpriteChanged +=
+ MainEditor_SpriteDataSelectedIndexChanged;
+ MainEditor.ObjectData_DataReset += MainEditor_ObjectData_DataReset;
+ MainEditor.ObjectData_ItemAdded += MainEditor_ObjectData_ItemAdded;
+ MainEditor.ObjectData_ItemEdited += MainEditor_ObjectData_ItemEdited;
+ MainEditor.ObjectData_ItemRemoved += MainEditor_ObjectData_ItemRemoved;
+ MainEditor.ObjectData_DataCleared += MainEditor_ObjectData_DataCleared;
+ MainEditor.AnimationFrameChanged += MainEditor_AnimationFrameChanged;
+
+ HeaderEditorView.AreaHeaderChanged += HeaderEditor_AreaHeaderChanged;
+
+ /*
+ ObjectListView.AddItem_Click += ObjectListView_AddItem_Click;
+ ObjectListView.DeleteItem_Click += ObjectListView_DeleteItem_Click;
+ ObjectListView.ClearItems_Click += ObjectListView_ClearItems_Click;
+ */
+
+ MainView.Player = MainEditor.Player;
+ MainView.PlayerState = MainEditor.PlayerState;
+ }
+
+ public bool AutoSaveEnabled
+ {
+ get
+ {
+ return MainEditor.AutoSaveEnabled;
+ }
+
+ set
+ {
+ MainEditor.AutoSaveEnabled = value;
+ }
+ }
+
+ public bool PruneAutoSavesEnabled
+ {
+ get
+ {
+ return MainEditor.PruneAutoSavesEnabled;
+ }
+
+ set
+ {
+ MainEditor.PruneAutoSavesEnabled = value;
+ }
+ }
+
+ public TimeSpan AutoSaveInterval
+ {
+ get
+ {
+ return MainEditor.AutoSaveInterval;
+ }
+
+ set
+ {
+ MainEditor.AutoSaveInterval = value;
+ }
+ }
+
+ public TimeSpan AutoSaveCutoffAge
+ {
+ get
+ {
+ return MainEditor.AutoSaveCutoffAge;
+ }
+
+ set
+ {
+ MainEditor.AutoSaveCutoffAge = value;
+ }
+ }
+
+ public bool AutoSaveHardCutoff
+ {
+ get
+ {
+ return MainEditor.AutoSaveHardCutoff;
+ }
+
+ set
+ {
+ MainEditor.AutoSaveHardCutoff = value;
+ }
+ }
+
+ private BrutarioEditor MainEditor { get; }
+
+ private IMainView MainView { get; }
+
+ private IExceptionView ExceptionHelper { get; }
+
+ private IFileNameSelector OpenFileNameSelector { get; }
+
+ private IFileNameSelector SaveFileNameSelector { get; }
+
+ private ISaveOnClosePrompt SaveOnClosePrompt { get; }
+
+ private IHeaderEditorView HeaderEditorView { get; }
+
+ private IObjectListView ObjectListView { get; }
+
+ private IObjectEditorView ObjectEditorView { get; }
+
+ private ISpriteEditorView SpriteEditorView { get; }
+
+ public void Open()
+ {
+ if (OpenFileNameSelector.Prompt() == PromptResult.Yes)
+ {
+ Open(OpenFileNameSelector.FileName!);
+ }
+ }
+
+ public void Open(string path)
+ {
+ if (!CleanupBeforeClose())
+ {
+ return;
+ }
+
+ try
+ {
+ MainEditor.Open(path);
+ }
+ catch
+ {
+ ExceptionHelper.Show("Failed to load ROM.");
+ }
+ }
+
+ public void Save()
+ {
+ while (true)
+ {
+ try
+ {
+ MainEditor.Save();
+ break;
+ }
+ catch
+ {
+ if (!ExceptionHelper.ShowAndPromptRetry("Failed to save ROM"))
+ {
+ break;
+ }
+ }
+ }
+ }
+
+ public void SaveAs()
+ {
+ if (SaveFileNameSelector.Prompt() == PromptResult.Yes)
+ {
+ SaveAs(SaveFileNameSelector.FileName!);
+ }
+ }
+
+ public void SaveAs(string path)
+ {
+ while (true)
+ {
+ try
+ {
+ MainEditor.SaveAs(path);
+ break;
+ }
+ catch
+ {
+ if (!ExceptionHelper.ShowAndPromptRetry("Failed to save ROM"))
+ {
+ break;
+ }
+ }
+ }
+ }
+
+ public void AutoSave()
+ {
+ if (MainEditor.IsOpen)
+ {
+ MainEditor.TryAutoSave();
+ }
+ }
+
+ public bool Close()
+ {
+ return CloseInternal();
+ }
+
+ public void Undo()
+ {
+ MainEditor.Undo();
+ }
+
+ public void Redo()
+ {
+ MainEditor.Redo();
+ }
+
+ public void CutCurrentItem()
+ {
+ MainEditor.CutCurrentItem();
+ }
+
+ public void CopyCurrentItem()
+ {
+ MainEditor.CopyCurrentItem();
+ }
+
+ public void Paste()
+ {
+ MainEditor.Paste();
+ }
+
+ public void AddItem()
+ {
+ if (MainView.SpriteMode)
+ {
+ AddSprite();
+ }
+ else
+ {
+ AddObject();
+ }
+ }
+
+ public void RemoveCurrentItem()
+ {
+ MainEditor.RemoveCurrentItem();
+ }
+
+ public void DeleteAllItems()
+ {
+ MainEditor.RemoveAllItems();
+ }
+
+ public void LoadAreaByLevel()
+ {
+ throw new NotImplementedException();
+ }
+
+ public bool IsValidAreaNumber(int areaNumber)
+ {
+ return MainEditor.IsValidAreaNumber(areaNumber);
+ }
+
+ public void JumpToArea(int areaNumber)
+ {
+ throw new NotImplementedException();
+ }
+
+ public void ExportTileData()
+ {
+ throw new NotImplementedException();
+ }
+
+ public void EditHeader()
+ {
+ var oldHeader = MainEditor.AreaHeader;
+ HeaderEditorView.AreaHeader = MainEditor.AreaHeader;
+
+ var result = HeaderEditorView.Prompt();
+ MainEditor.FinishSetAreaHeader(oldHeader, commit: result);
+ }
+
+ public void ToggleSpritMode(bool enabled)
+ {
+ MainEditor.SpriteMode = enabled;
+ }
+
+ public void SetPlayer(Player player)
+ {
+ MainEditor.Player = player;
+ }
+
+ public void SetPlayerState(PlayerState playerState)
+ {
+ MainEditor.PlayerState = playerState;
+ }
+
+ public void SetAreaNumber(int areaNumber)
+ {
+ if (!CleanupBeforeClose())
+ {
+ return;
+ }
+
+ MainEditor.AreaNumber = areaNumber;
+ }
+
+ public void SetStartX(int startX)
+ {
+ MainEditor.StartX = startX;
+ }
+
+ public void SetSelectedItem(int x, int y)
+ {
+ MainEditor.SetSelectedItem(x, y);
+ }
+
+ public void EditSelectedItem()
+ {
+ if (MainEditor.SpriteMode)
+ {
+ EditSelectedSprite();
+ }
+ else
+ {
+ EditSelectedObject();
+ }
+ }
+
+ public void EditSelectedObject()
+ {
+ if (MainEditor.SelectedObjectIndex == -1)
+ {
+ return;
+ }
+
+ ObjectEditorView.AreaObjectCommand = MainEditor.SelectedObject;
+ ObjectEditorView.AreaObjectCommandChanged += ObjectEditor_ItemChanged;
+
+ var commit = ObjectEditorView.PromptConfirm();
+ MainEditor.FinishEditObject(commit);
+
+ ObjectEditorView.AreaObjectCommandChanged -= ObjectEditor_ItemChanged;
+
+ void ObjectEditor_ItemChanged(object? sender, EventArgs e)
+ {
+ MainEditor.EditPreviewObject(
+ ObjectEditorView.AreaObjectCommand);
+ }
+ }
+
+ public void EditSelectedSprite()
+ {
+ if (MainEditor.SelectedSpriteIndex == -1)
+ {
+ return;
+ }
+
+ SpriteEditorView.AreaSpriteCommand = MainEditor.SelectedSprite;
+ SpriteEditorView.AreaSpriteCommandChanged += ItemChanged;
+
+ var commit = SpriteEditorView.PromptConfirm();
+ MainEditor.FinishEditSprite(commit);
+
+ void ItemChanged(object? sender, EventArgs e)
+ {
+ MainEditor.EditPreviewSprite(
+ SpriteEditorView.AreaSpriteCommand);
+ }
+ }
+
+ public void InitializeMoveItem()
+ {
+ MainEditor.InitializeEditItem();
+ }
+
+ public void InitializeMoveObject()
+ {
+ MainEditor.InitializeEditObject();
+ }
+
+ public void MoveSelectedItem(int x, int y)
+ {
+ MainEditor.MoveSelectedItem(x, y);
+ }
+
+ public void FinishEditItem(bool commit)
+ {
+ MainEditor.FinishEditItem(commit);
+ }
+
+ public void FinishMoveObject(bool commit)
+ {
+ MainEditor.FinishEditObject(commit);
+ }
+
+ public void FinishMoveSprite(bool commit)
+ {
+ MainEditor.FinishEditSprite(commit);
+ }
+
+ public void UpdateFrame(int frame)
+ {
+ MainEditor.AnimationFrame = frame;
+ }
+
+ public DrawData GetDrawData(
+ int startX,
+ Size size,
+ Color separatorColor,
+ Color passiveColor,
+ Color selectColor)
+ {
+ return MainEditor.GetDrawData(
+ startX,
+ size,
+ separatorColor,
+ passiveColor,
+ selectColor);
+ }
+
+ private void MainEditor_ObjectData_ItemAdded(object? sender, ItemAddedEventArgs e)
+ {
+ SetEditItemEnabled();
+ var item = MainEditor.ObjectData[e.Index];
+ //ObjectListView.Items.Insert(e.Index, item);
+ }
+
+ private void MainEditor_ObjectData_ItemEdited(object? sender, ObjectEditedEventArgs e)
+ {
+ //ObjectListView.Items.RemoveAt(e.OldIndex);
+ //ObjectListView.Items.Insert(e.NewIndex, e.NewCommand);
+ }
+
+ private void MainEditor_ObjectData_DataCleared(object? sender, EventArgs e)
+ {
+ SetDeleteAllEnabled();
+ //ObjectListView.Items.Clear();
+ }
+
+ private void MainEditor_HasUnsavedChangesChanged(object? sender, EventArgs e)
+ {
+ MainView.SaveEnabled = MainEditor.HasUnsavedChanges;
+ SetName();
+ }
+
+ private void MainEditor_ObjectData_DataReset(object? sender, EventArgs e)
+ {
+ MainView.DeleteAllEnabled = MainEditor.ObjectData.Count > 0;
+ /*
+ ObjectListView.Items.Clear();
+ foreach (var item in MainEditor.ObjectData)
+ {
+ ObjectListView.Items.Add(item);
+ }
+ */
+ }
+
+ private void MainEditor_ItemCopied(object? sender, EventArgs e)
+ {
+ MainView.PasteEnabled = true;
+ }
+
+ private void MainEditor_AreaLoaded(object? sender, EventArgs e)
+ {
+ MainView.MapEditorEnabled = true;
+ }
+
+ private void MainEditor_AnimationFrameChanged(object? sender, EventArgs e)
+ {
+ MainView.Redraw();
+ }
+
+ private void MainEditor_ObjectData_ItemRemoved(object? sender, ItemAddedEventArgs e)
+ {
+ SetEditItemEnabled();
+ SetDeleteAllEnabled();
+ //ObjectListView.Items.RemoveAt(e.Index);
+ }
+
+ private void MainEditor_PathChanged(object? sender, EventArgs e)
+ {
+ SetName();
+ }
+
+ private void MainEditor_AreaHeaderChanged(object? sender, EventArgs e)
+ {
+ //ObjectListView.AreaPlatformType =
+ ObjectEditorView.AreaPlatformType = MainEditor.AreaHeader.AreaPlatformType;
+ }
+
+ private void MainEditor_ObjectDataSelectedIndexChanged(object? sender, EventArgs e)
+ {
+ SetEditItemEnabled();
+ }
+
+ private void MainEditor_SpriteDataSelectedIndexChanged(object? sender, EventArgs e)
+ {
+ SetEditItemEnabled();
+ }
+
+ private void MainEditor_HistoryCleared(object? sender, EventArgs e)
+ {
+ SetUndoRedoEnabled();
+ }
+
+ private void MainEditor_UndoElementAdded(object? sender, EventArgs e)
+ {
+ SetUndoRedoEnabled();
+ }
+
+ private void MainEditor_UndoComplete(object? sender, EventArgs e)
+ {
+ SetUndoRedoEnabled();
+ }
+
+ private void MainEditor_RedoComplete(object? sender, EventArgs e)
+ {
+ SetUndoRedoEnabled();
+ }
+
+ private void SetUndoRedoEnabled()
+ {
+ MainView.UndoEnabled = MainEditor.CanUndo;
+ MainView.RedoEnabled = MainEditor.CanRedo;
+ }
+
+ private void MainEditor_StartXChanged(object? sender, EventArgs e)
+ {
+ MainView.StartX = MainEditor.StartX;
+ }
+
+ private void MainEditor_Invalidated(object? sender, EventArgs e)
+ {
+ MainView.Redraw();
+ }
+
+ private void SpriteEditor_AreaSpriteCommandChanged(object? sender, EventArgs e)
+ {
+ /*
+ MainModel.SpriteData[MainModel.CurrentSpriteIndex] =
+ SpriteEditor.AreaSpriteCommand;
+ */
+ }
+
+ private bool CloseInternal()
+ {
+ if (!CleanupBeforeClose())
+ {
+ return false;
+ }
+
+ MainEditor.Close();
+ return true;
+ }
+
+ private bool CleanupBeforeClose()
+ {
+ if (MainEditor.HasUnsavedChanges)
+ {
+ switch (SaveOnClosePrompt.Prompt())
+ {
+ case PromptResult.Yes:
+ MainEditor.Save();
+ break;
+
+ case PromptResult.Cancel:
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ ///
+ /// Inserts a temporary object at the current user coordinates. A dialog is
+ /// opened for the user to edit the object. The user may confirm these edits
+ /// and the object will be committed, or the user may cancel the add and the
+ /// temporary object will be removed (no changes to the undo history will be
+ /// made in this event).
+ ///
+ private void AddObject()
+ {
+ // First we take whatever the current object is, and just add it a
+ // second time.
+ var item = MainEditor.DefaultPreviewObject;
+ MainEditor.AddPreviewObject(item);
+
+ ObjectEditorView.AreaObjectCommand = item;
+ ObjectEditorView.AreaObjectCommandChanged += ObjectEditor_ItemChanged;
+
+ var commit = ObjectEditorView.PromptConfirm();
+ MainEditor.FinishAddObject(commit);
+
+ ObjectEditorView.AreaObjectCommandChanged -= ObjectEditor_ItemChanged;
+
+ void ObjectEditor_ItemChanged(object? sender, EventArgs e)
+ {
+ MainEditor.EditPreviewObject(
+ ObjectEditorView.AreaObjectCommand);
+ }
+ }
+
+ private void AddSprite()
+ {
+ var item = MainEditor.DefaultPreviewSprite;
+ MainEditor.AddPreviewSprite(item);
+
+ SpriteEditorView.AreaSpriteCommand = item;
+ SpriteEditorView.AreaSpriteCommandChanged += SpriteEditor_ItemChanged;
+
+ var commit = SpriteEditorView.PromptConfirm();
+ MainEditor.FinishAddSprite(commit);
+
+ SpriteEditorView.AreaSpriteCommandChanged -= SpriteEditor_ItemChanged;
+
+ void SpriteEditor_ItemChanged(object? sender, EventArgs e)
+ {
+ MainEditor.EditPreviewSprite(
+ SpriteEditorView.AreaSpriteCommand);
+ }
+ }
+
+ private void SetName()
+ {
+ var sb = new StringBuilder("Brutario");
+ if (MainEditor.IsOpen)
+ {
+ _ = sb.Append(" - ");
+ _ = sb.Append(MainEditor.Path);
+ if (MainEditor.HasUnsavedChanges)
+ {
+ _ = sb.Append('*');
+ }
+ }
+
+ MainView.Title = sb.ToString();
+ }
+
+ private void HeaderEditor_AreaHeaderChanged(object? sender, EventArgs e)
+ {
+ MainEditor.InitializeSetAreaHeader(HeaderEditorView.AreaHeader);
+ }
+
+ private void MainEditor_FileOpened(object? sender, EventArgs e)
+ {
+ MainView.EditorEnabled = true;
+ }
+
+ private void MainEditor_FileSaved(object? sender, EventArgs e)
+ {
+ MainView.SaveEnabled = false;
+ }
+
+ private void MainEditor_FileClosed(object? sender, EventArgs e)
+ {
+ MainView.EditorEnabled =
+ MainView.MapEditorEnabled =
+ MainView.SaveEnabled =
+ MainView.UndoEnabled =
+ MainView.RedoEnabled =
+ MainView.EditItemEnabled =
+ MainView.PasteEnabled =
+ MainView.DeleteAllEnabled = false;
+ }
+
+ private void MainEditor_AreaNumberChanged(object? sender, EventArgs e)
+ {
+ MainView.AreaNumber = MainEditor.AreaNumber;
+ }
+
+ private void MainEditor_SpriteModeChanged(object? sender, EventArgs e)
+ {
+ MainView.SpriteMode = MainEditor.SpriteMode;
+ SetEditItemEnabled();
+ SetDeleteAllEnabled();
+ SetPasteEnabled();
+ }
+
+ private void SetEditItemEnabled()
+ {
+ MainView.EditItemEnabled = MainEditor.SpriteMode
+ ? MainEditor.SelectedSpriteIndex != -1
+ : MainEditor.SelectedObjectIndex != -1;
+ }
+
+ private void SetDeleteAllEnabled()
+ {
+ MainView.DeleteAllEnabled = MainEditor.SpriteMode
+ ? MainEditor.SpriteData.Count > 0
+ : MainEditor.ObjectData.Count > 0;
+ }
+
+ private void SetPasteEnabled()
+ {
+ MainView.PasteEnabled = MainEditor.SpriteMode
+ ? MainEditor.CopiedSprite is not null
+ : MainEditor.CopiedObject is not null;
+ }
+
+ private void MainEditor_PlayerChanged(object? sender, EventArgs e)
+ {
+ MainView.Player = MainEditor.Player;
+ }
+
+ private void MainEditor_PlayerStateChanged(object? sender, EventArgs e)
+ {
+ MainView.PlayerState = MainEditor.PlayerState;
+ }
+
+ private void ObjectListView_AddItem_Click(object? sender, EventArgs e)
+ {
+ AddObject();
+ }
+
+ private void ObjectListView_DeleteItem_Click(object? sender, EventArgs e)
+ {
+ MainEditor.RemoveObjectAt(ObjectListView.SelectedIndex);
+ }
+
+ private void ObjectListView_ClearItems_Click(object? sender, EventArgs e)
+ {
+ MainEditor.ClearObjects();
+ }
+}
diff --git a/src/Brutario.Core/SortedObjectListEditor.cs b/src/Brutario.Core/SortedObjectListEditor.cs
new file mode 100644
index 0000000..ec38f68
--- /dev/null
+++ b/src/Brutario.Core/SortedObjectListEditor.cs
@@ -0,0 +1,407 @@
+namespace Brutario.Core;
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Linq;
+using System.Reflection;
+
+using Maseya.Smas.Smb1.AreaData.ObjectData;
+
+///
+/// TLDR: There are fancy structures I could use, but the objects are displayed
+/// in text as an array, so I'm stuck with using a simple array.
+///
+/// I spent a lot of time wondering which data structure should house the object
+/// data. The data must remain sorted, so I actually wrote a whole red-black
+/// tree structure for this purpose. However, "sorting" this object data is
+/// actually quite a vague task. Data can come in any order, and the X
+/// coordinates _can_ go backwards, but then it's not rendered right. For
+/// practical purposes, let's say that X is strictly increasing (this will stop
+/// being true when I rewrite the level engine). Then yes, X is a good sorting
+/// key, but multiple objects can exists on the same X.
+///
+/// In this scenario, we need a subsort. At first, I thought sorting by Y would
+/// be okay, but multiple objects can even exist on the same Y, and there are
+/// scenarios in vanilla smb1 that would break if we tried to order Y, so this
+/// is out. This leaves us with creating a Z buffer. Objects on the same X are
+/// subsorted by a Z buffer which is defined as the order they appear in the
+/// object data. Simple enough.
+///
+/// But now the question becomes whether the Z buffer should be an explicit
+/// variable, or implicitly defined from its index. If I were to do a red-black
+/// tree, then Z would need to be a variable. The red-black tree starts to fall
+/// apart now, because if I want to move objects around, then I'll need to edit
+/// all of the Z variables on the X. It quickly gets messy, and is O(n logn) in
+/// worst case (not that runtime is crucial on such a small scale as a SNES
+/// game).
+///
+/// I then considered bucket sorting. An smb1 level is 0x200 tiles wide at max,
+/// so I could 0x200 array of sorted object lists. Each object list would be
+/// very small (5 or less elements), so sorting them is easy, and moving objects
+/// around is very easy as I just index to the X coordinate. Iteration would be
+/// a chore though, as I need to go through all 0x200 buckets (which isn't bad
+/// really). But getting the index of an object would be O(n). Would I ever need
+/// the index here though? There's going to be a UI array, which is my concern.
+///
+/// The objects will be visible in text window as an array of strings. Whatever
+/// data structure I use will need to work nicely with this. For example, if a
+/// user clicks on an object in the graphic window, then I need to find that
+/// object in the bucket, which should be O(1) since I know the X coordinate.
+/// And I also need the object in the UI window, is O(log n) since the UI array
+/// will always be sorted. Adding and removing objects would be O(1) in the
+/// bucket, but still O(n) in the UI array, so it's O(n) overall.
+///
+/// End of the day, what's the point of having an optimized data structure, if
+/// it's still dominated by a less optimized structure that I need to use
+///
+public class SortedObjectListEditor :
+ IList,
+ IReadOnlyList
+{
+ public SortedObjectListEditor()
+ {
+ Items = new List();
+ }
+
+ public SortedObjectListEditor(IEnumerable commands)
+ {
+ Items = new List(GetSortedUICommands(commands));
+ }
+
+ public event EventHandler? DataReset;
+
+ public event EventHandler? ItemEdited;
+
+ public event EventHandler? ItemAdded;
+
+ public event EventHandler? ItemRemoved;
+
+ public event EventHandler? DataCleared;
+
+ public int Count
+ {
+ get
+ {
+ return Items.Count;
+ }
+ }
+
+ public UIAreaObjectCommand this[int index]
+ {
+ get
+ {
+ return Items[index];
+ }
+ set
+ {
+ _ = Edit(index, value);
+ }
+ }
+
+ bool ICollection.IsReadOnly
+ {
+ get { return false; }
+ }
+
+ private List Items
+ {
+ get;
+ }
+
+ public static IEnumerable GetAreaData(
+ IEnumerable bytes)
+ {
+ using var en = bytes.GetEnumerator();
+ if (!en.MoveNext())
+ {
+ throw new ArgumentException("Area object data ended early.");
+ }
+
+ while (en.Current != AreaObjectCommand.TerminationCode)
+ {
+ if (AreaObjectCommand.IsThreeByteSpecifier(en.Current))
+ {
+ var list = new List(GetBytes(3));
+ yield return new AreaObjectCommand(
+ list[0],
+ list[1],
+ list[2]);
+ }
+ else
+ {
+ var list = new List(GetBytes(2));
+ yield return new AreaObjectCommand(
+ list[0],
+ list[1]);
+ }
+ }
+
+ IEnumerable GetBytes(int size)
+ {
+ for (var i = 0; i < size; i++)
+ {
+ yield return en.Current;
+
+ if (!en.MoveNext())
+ {
+ throw new ArgumentException("Area object data ended early.");
+ }
+ }
+ }
+ }
+
+ public void Reset(IEnumerable commands)
+ {
+ Reset(GetUICommands(commands));
+ }
+
+ public void Reset(IEnumerable commands)
+ {
+ Items.Clear();
+ foreach (var item in commands.OrderBy(x => x.X))
+ {
+ if (item.Command.Code == AreaObjectCode.ScreenJump)
+ {
+ continue;
+ }
+
+ Items.Add(item);
+ }
+
+ DataReset?.Invoke(this, EventArgs.Empty);
+ }
+
+ public bool Contains(UIAreaObjectCommand command)
+ {
+ return IndexOf(command) >= 0;
+ }
+
+ public int IndexOf(UIAreaObjectCommand item)
+ {
+ var result = SearchFirstIndexOf(item);
+ for (; result < Count; result++)
+ {
+ if (item == Items[result])
+ {
+ return result;
+ }
+ }
+
+ return -1;
+ }
+
+ public int AtCoords(int x, int y)
+ {
+ if (Items.Count == 0)
+ {
+ return -1;
+ }
+
+ var command = Items[0];
+ command.X = x;
+ var index = SearchFirstIndexOf(command);
+ if (index == Count)
+ {
+ return -1;
+ }
+
+ for (; index < Items.Count && Items[index].X == x; index++)
+ {
+ if (Items[index].Y == y)
+ {
+ return index;
+ }
+ }
+
+ return -1;
+ }
+
+ public int Add(AreaObjectCommand command, int page)
+ {
+ return Add(new UIAreaObjectCommand(command, page));
+ }
+
+ public int Add(UIAreaObjectCommand command)
+ {
+ var index = AddInternal(command);
+ ItemAdded?.Invoke(this, new ItemAddedEventArgs(index));
+ return index;
+ }
+
+ void ICollection.Add(UIAreaObjectCommand command)
+ {
+ _ = Add(command);
+ }
+
+ public bool Remove(UIAreaObjectCommand item)
+ {
+ var index = IndexOf(item);
+ if (index != -1)
+ {
+ RemoveAt(index);
+ return true;
+ }
+
+ return false;
+ }
+
+ public void RemoveAt(int index)
+ {
+ Items.RemoveAt(index);
+ ItemRemoved?.Invoke(this, new ItemAddedEventArgs(index));
+ }
+
+ public int Edit(int index, UIAreaObjectCommand newItem)
+ {
+ if (Items[index] == newItem)
+ {
+ return index;
+ }
+
+ var oldItem = Items[index];
+ Items.RemoveAt(index);
+ var newIndex = AddInternal(newItem);
+
+ ItemEdited?.Invoke(
+ this,
+ new ObjectEditedEventArgs(index, newIndex, oldItem, newItem));
+ return newIndex;
+ }
+
+ public void Clear()
+ {
+ Items.Clear();
+ DataCleared?.Invoke(this, EventArgs.Empty);
+ }
+
+ void IList.Insert(
+ int index,
+ Brutario.Core.UIAreaObjectCommand item)
+ {
+ _ = Add(item);
+ }
+
+ public IEnumerator GetEnumerator()
+ {
+ return Items.GetEnumerator();
+ }
+
+ System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
+ {
+ return GetEnumerator();
+ }
+
+ public IEnumerable GetObjectData()
+ {
+ var lastPage = 0;
+ foreach (var command in Items)
+ {
+ var page = command.Page;
+ var item = command.Command;
+ if (page <= lastPage + 1)
+ {
+ Debug.Assert(page >= lastPage);
+
+ item.ScreenFlag = page != lastPage;
+ }
+ else
+ {
+ yield return new AreaObjectCommand(0x0D, (byte)(page & 0x1F));
+ item.ScreenFlag = false;
+ }
+
+ lastPage = page;
+ yield return item;
+ }
+ }
+
+ public byte[] ToByteArray()
+ {
+ var result = new List();
+ foreach (var command in GetObjectData())
+ {
+ result.Add(command.Value1);
+ result.Add(command.Value2);
+ if (command.IsThreeByteCommand)
+ {
+ result.Add(command.Value3);
+ }
+ }
+
+ result.Add(AreaObjectCommand.TerminationCode);
+
+ return result.ToArray();
+ }
+
+ void ICollection.CopyTo(
+ UIAreaObjectCommand[] array,
+ int index)
+ {
+ if ((uint)index >= (uint)array.Length)
+ {
+ throw new ArgumentOutOfRangeException(nameof(index));
+ }
+
+ if (index + Count < array.Length)
+ {
+ throw new ArgumentOutOfRangeException(nameof(index));
+ }
+
+ foreach ((var i, var value) in Enumerable.Range(0, Count).Zip(this))
+ {
+ array[index + i] = value;
+ }
+ }
+
+ private static IEnumerable GetUICommands(
+ IEnumerable data)
+ {
+ var page = 0;
+ foreach (var item in data)
+ {
+ if (item.ScreenFlag)
+ {
+ page++;
+ }
+ else if (item.Code == AreaObjectCode.ScreenJump)
+ {
+ page = item.BaseCommand & 0x1F;
+ continue;
+ }
+
+ yield return new UIAreaObjectCommand(item, page);
+ }
+ }
+
+ private static IEnumerable GetSortedUICommands(
+ IEnumerable data)
+ {
+ return GetUICommands(data).OrderBy(item => item.X);
+ }
+
+ private int AddInternal(UIAreaObjectCommand item)
+ {
+ if (item.Command.Code == AreaObjectCode.ScreenJump)
+ {
+ return -1;
+ }
+
+ var index = SearchLastIndexOf(item);
+ Items.Insert(index, item);
+ return index;
+ }
+
+ private int SearchFirstIndexOf(UIAreaObjectCommand item)
+ {
+ var comparer = Comparer.Create(
+ (x, y) => x.X < y.X ? -1 : +1);
+ return ~Items.BinarySearch(item, comparer);
+ }
+
+ private int SearchLastIndexOf(UIAreaObjectCommand item)
+ {
+ var comparer = Comparer.Create(
+ (x, y) => x.X <= y.X ? -1 : +1);
+ return ~Items.BinarySearch(item, comparer);
+ }
+}
diff --git a/src/Brutario.Core/SortedSpriteListEditor.cs b/src/Brutario.Core/SortedSpriteListEditor.cs
new file mode 100644
index 0000000..b29c5f0
--- /dev/null
+++ b/src/Brutario.Core/SortedSpriteListEditor.cs
@@ -0,0 +1,354 @@
+namespace Brutario.Core;
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Linq;
+
+using Maseya.Smas.Smb1.AreaData.SpriteData;
+
+public class SortedSpriteListEditor :
+ IList,
+ IReadOnlyList
+{
+ public SortedSpriteListEditor()
+ {
+ Items = new List();
+ }
+
+ public SortedSpriteListEditor(IEnumerable commands)
+ {
+ Items = new List(GetSortedUICommands(commands));
+ }
+
+ public event EventHandler? DataReset;
+
+ public event EventHandler? ItemEdited;
+
+ public event EventHandler? ItemAdded;
+
+ public event EventHandler? ItemRemoved;
+
+ public event EventHandler? DataCleared;
+
+ public int Count
+ {
+ get
+ {
+ return Items.Count;
+ }
+ }
+
+ public UIAreaSpriteCommand this[int index]
+ {
+ get
+ {
+ return Items[index];
+ }
+ set
+ {
+ _ = Edit(index, value);
+ }
+ }
+
+ bool ICollection.IsReadOnly
+ {
+ get { return false; }
+ }
+
+ private List Items
+ {
+ get;
+ }
+
+ public static IEnumerable GetAreaData(
+ IEnumerable bytes)
+ {
+ using var en = bytes.GetEnumerator();
+ if (!en.MoveNext())
+ {
+ throw new ArgumentException("Area object data ended early.");
+ }
+
+ while (en.Current != AreaSpriteCommand.TerminationCode)
+ {
+ if (AreaSpriteCommand.IsThreeByteSpecifier(en.Current))
+ {
+ var list = new List(GetBytes(3));
+ yield return new AreaSpriteCommand(
+ list[0],
+ list[1],
+ list[2]);
+ }
+ else
+ {
+ var list = new List(GetBytes(2));
+ yield return new AreaSpriteCommand(
+ list[0],
+ list[1]);
+ }
+ }
+
+ IEnumerable GetBytes(int size)
+ {
+ for (var i = 0; i < size; i++)
+ {
+ yield return en.Current;
+
+ if (!en.MoveNext())
+ {
+ throw new ArgumentException("Area object data ended early.");
+ }
+ }
+ }
+ }
+
+ public void Reset(IEnumerable commands)
+ {
+ Reset(GetUICommands(commands));
+ }
+
+ public void Reset(IEnumerable commands)
+ {
+ Items.Clear();
+ foreach (var item in commands.OrderBy(x => x.X))
+ {
+ if (item.Command.Code == AreaSpriteCode.ScreenJump)
+ {
+ continue;
+ }
+
+ Items.Add(item);
+ }
+
+ DataReset?.Invoke(this, EventArgs.Empty);
+ }
+
+ public bool Contains(UIAreaSpriteCommand command)
+ {
+ return IndexOf(command) >= 0;
+ }
+
+ public int IndexOf(UIAreaSpriteCommand item)
+ {
+ var result = IndexOf(item);
+ return result < 0 ? -1 : result;
+ }
+
+ public int AtCoords(int x, int y)
+ {
+ if (Items.Count == 0)
+ {
+ return -1;
+ }
+
+ var command = Items[0];
+ command.X = x;
+ var index = SearchFirstIndexOf(command);
+ if (index == Count)
+ {
+ return -1;
+ }
+
+ for (; index < Items.Count && Items[index].X == x; index++)
+ {
+ if (Items[index].Y == y)
+ {
+ return index;
+ }
+ }
+
+ return -1;
+ }
+
+ public int Add(AreaSpriteCommand command, int page)
+ {
+ return Add(new UIAreaSpriteCommand(command, page));
+ }
+
+ public int Add(UIAreaSpriteCommand command)
+ {
+ var index = AddInternal(command);
+ ItemAdded?.Invoke(this, EventArgs.Empty);
+ return index;
+ }
+
+ void ICollection.Add(UIAreaSpriteCommand item)
+ {
+ _ = Add(item);
+ }
+
+ public bool Remove(UIAreaSpriteCommand item)
+ {
+ var index = IndexOf(item);
+ if (index != -1)
+ {
+ RemoveAt(index);
+ return true;
+ }
+
+ return false;
+ }
+
+ public void RemoveAt(int index)
+ {
+ Items.RemoveAt(index);
+ ItemRemoved?.Invoke(this, EventArgs.Empty);
+ }
+
+ public int Edit(int index, UIAreaSpriteCommand newItem)
+ {
+ Items.RemoveAt(index);
+ index = AddInternal(newItem);
+ ItemEdited?.Invoke(this, EventArgs.Empty);
+ return index;
+ }
+
+ public void Clear()
+ {
+ Items.Clear();
+ DataCleared?.Invoke(this, EventArgs.Empty);
+ }
+
+ void IList.Insert(
+ int index,
+ Brutario.Core.UIAreaSpriteCommand item)
+ {
+ Add(item);
+ }
+
+ public IEnumerator GetEnumerator()
+ {
+ return Items.GetEnumerator();
+ }
+
+ System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
+ {
+ return GetEnumerator();
+ }
+
+ public IEnumerable GetSpriteData()
+ {
+ var lastPage = 0;
+ foreach (var command in Items)
+ {
+ var page = command.Page;
+ var item = command.Command;
+ if (page <= lastPage + 1)
+ {
+ Debug.Assert(page >= lastPage);
+
+ item.ScreenFlag = page != lastPage;
+ }
+ else if (page > lastPage + 1)
+ {
+ yield return new AreaSpriteCommand(0x0F, (byte)(page & 0x1F));
+ item.ScreenFlag = false;
+ }
+
+ lastPage = page;
+ yield return item;
+ }
+ }
+
+ public byte[] ToByteArray()
+ {
+ var result = new List();
+ foreach (var command in GetSpriteData())
+ {
+ result.Add(command.Value1);
+ result.Add(command.Value2);
+ if (command.IsThreeByteCommand)
+ {
+ result.Add(command.Value3);
+ }
+ }
+
+ result.Add(AreaSpriteCommand.TerminationCode);
+
+ return result.ToArray();
+ }
+
+ void ICollection.CopyTo(
+ UIAreaSpriteCommand[] array,
+ int index)
+ {
+ if ((uint)index >= (uint)array.Length)
+ {
+ throw new ArgumentOutOfRangeException(nameof(index));
+ }
+
+ if (index + Count < array.Length)
+ {
+ throw new ArgumentOutOfRangeException(nameof(index));
+ }
+
+ foreach ((var i, var value) in Enumerable.Range(0, Count).Zip(this))
+ {
+ array[index + i] = value;
+ }
+ }
+
+ private static IEnumerable GetUICommands(
+ IEnumerable data)
+ {
+ var page = 0;
+ foreach (var item in data)
+ {
+ if (item.ScreenFlag)
+ {
+ page++;
+ }
+ else if (item.Code == AreaSpriteCode.ScreenJump)
+ {
+ page = item.BaseCommand & 0x1F;
+ continue;
+ }
+
+ yield return new UIAreaSpriteCommand(item, page);
+ }
+ }
+
+ private static IEnumerable GetSortedUICommands(
+ IEnumerable data)
+ {
+ return GetUICommands(data).OrderBy(item => item.X);
+ }
+
+ private int AddInternal(UIAreaSpriteCommand item)
+ {
+ if (item.Command.Code == AreaSpriteCode.ScreenJump)
+ {
+ return -1;
+ }
+
+ var index = SearchLastIndexOf(item);
+ Items.Insert(index, item);
+ return index;
+ }
+
+ private bool RemoveInternal(UIAreaSpriteCommand item)
+ {
+ var index = IndexOf(item);
+ if (index < 0)
+ {
+ return false;
+ }
+
+ Items.RemoveAt(index);
+ return true;
+ }
+
+ private int SearchFirstIndexOf(UIAreaSpriteCommand item)
+ {
+ var comparer = Comparer.Create(
+ (x, y) => x.X < y.X ? -1 : +1);
+ return ~Items.BinarySearch(item, comparer);
+ }
+
+ private int SearchLastIndexOf(UIAreaSpriteCommand item)
+ {
+ var comparer = Comparer.Create(
+ (x, y) => x.X <= y.X ? -1 : +1);
+ return ~Items.BinarySearch(item, comparer);
+ }
+}
diff --git a/src/Brutario.Core/UIAreaObjectCommand.cs b/src/Brutario.Core/UIAreaObjectCommand.cs
new file mode 100644
index 0000000..deb47c9
--- /dev/null
+++ b/src/Brutario.Core/UIAreaObjectCommand.cs
@@ -0,0 +1,116 @@
+//
+// Copyright (c) 2022 spel werdz rite. All rights reserved. Licensed under GNU Affero
+// General Public License. See LICENSE in project root for full license
+// information, or visit https://www.gnu.org/licenses/#AGPL
+//
+
+namespace Brutario.Core;
+
+using System;
+using System.Drawing;
+
+using Maseya.Smas.Smb1.AreaData.ObjectData;
+
+public struct UIAreaObjectCommand : IEquatable
+{
+ private AreaObjectCommand command_;
+
+ public UIAreaObjectCommand(AreaObjectCommand command, int page, int z = 0)
+ {
+ command_ = command;
+ Page = page;
+ Z = z;
+ }
+
+ public AreaObjectCommand Command
+ {
+ get
+ {
+ return command_;
+ }
+
+ set
+ {
+ command_ = value;
+ }
+ }
+
+ public int Page { get; set; }
+
+ public int X
+ {
+ get
+ {
+ return Command.X | (Page << 4);
+ }
+
+ set
+ {
+ Page = value >> 4;
+ command_.X = value & 0x0F;
+ }
+ }
+
+ public int Y
+ {
+ get
+ {
+ return command_.Y;
+ }
+
+ set
+ {
+ command_.Y = value;
+ }
+ }
+
+ public Rectangle SelectionRectangle
+ {
+ get
+ {
+ return new Rectangle(X << 4, (Y + 2) << 4, 0x10, 0x10);
+ }
+ }
+
+ public int Z { get; set; }
+
+ public string HexString
+ {
+ get
+ {
+ return $"{Page:X2} {Command.HexString}";
+ }
+ }
+
+ public static bool operator ==(UIAreaObjectCommand left, UIAreaObjectCommand right)
+ {
+ return left.Equals(right);
+ }
+
+ public static bool operator !=(UIAreaObjectCommand left, UIAreaObjectCommand right)
+ {
+ return !(left == right);
+ }
+
+ public bool Equals(UIAreaObjectCommand other)
+ {
+ return Command.Equals(other.Command)
+ && Page.Equals(other.Page)
+ && Z.Equals(other.Z);
+ }
+
+ public override bool Equals(object? obj)
+ {
+ return obj is UIAreaObjectCommand other && Equals(other);
+ }
+
+ public override int GetHashCode()
+ {
+ return HashCode.Combine(Command, Page, Z);
+ }
+
+ public override string ToString()
+ {
+ return $"{X}, {Y}, {Z}, {Command.BaseName}";
+ }
+}
diff --git a/src/Brutario.Core/UIAreaSpriteCommand.cs b/src/Brutario.Core/UIAreaSpriteCommand.cs
new file mode 100644
index 0000000..a1771e8
--- /dev/null
+++ b/src/Brutario.Core/UIAreaSpriteCommand.cs
@@ -0,0 +1,120 @@
+//
+// Copyright (c) 2022 spel werdz rite. All rights reserved. Licensed under GNU Affero
+// General Public License. See LICENSE in project root for full license
+// information, or visit https://www.gnu.org/licenses/#AGPL
+//
+
+namespace Brutario.Core;
+
+using System;
+using System.Drawing;
+
+using Maseya.Smas.Smb1.AreaData.SpriteData;
+
+public struct UIAreaSpriteCommand : IEquatable
+{
+ private AreaSpriteCommand command_;
+
+ public UIAreaSpriteCommand(AreaSpriteCommand command, int page, int z = 0)
+ {
+ command_ = command;
+ Page = page;
+ Z = z;
+ }
+
+ public AreaSpriteCommand Command
+ {
+ get
+ {
+ return command_;
+ }
+
+ set
+ {
+ command_ = value;
+ }
+ }
+
+ public int Page { get; set; }
+
+ public int X
+ {
+ get
+ {
+ return Command.X | (Page << 4);
+ }
+
+ set
+ {
+ Page = value >> 4;
+ command_.X = value & 0x0F;
+ }
+ }
+
+ public int Z { get; set; }
+
+ public int Y
+ {
+ get
+ {
+ return command_.Y;
+ }
+
+ set
+ {
+ command_.Y = value;
+ }
+ }
+
+ public Rectangle SelectionRectangle
+ {
+ get
+ {
+ return new Rectangle(X << 4, Y << 4, 0x10, 0x10);
+ }
+ }
+
+ public string HexString
+ {
+ get
+ {
+ return $"{Page:X2} {Command.HexString}";
+ }
+ }
+
+ public static bool operator ==(
+ UIAreaSpriteCommand left,
+ UIAreaSpriteCommand right)
+ {
+ return left.Equals(right);
+ }
+
+ public static bool operator !=(
+ UIAreaSpriteCommand left,
+ UIAreaSpriteCommand right)
+ {
+ return !(left == right);
+ }
+
+ public bool Equals(UIAreaSpriteCommand other)
+ {
+ return Command.Equals(other.Command)
+ && Page.Equals(other.Page)
+ && Z.Equals(other.Z);
+ }
+
+ public override bool Equals(object? obj)
+ {
+ return obj is UIAreaSpriteCommand other && Equals(other);
+ }
+
+ public override int GetHashCode()
+ {
+ return HashCode.Combine(Command, Page, Z);
+ }
+
+ public override string ToString()
+ {
+ return $"{X}, {Y}, {Z}, {Command.FullName}";
+ }
+}
diff --git a/src/Brutario.Core/UndoElement.cs b/src/Brutario.Core/UndoElement.cs
new file mode 100644
index 0000000..7251a2f
--- /dev/null
+++ b/src/Brutario.Core/UndoElement.cs
@@ -0,0 +1,28 @@
+//
+// Copyright (c) 2022 spel werdz rite. All rights reserved. Licensed under GNU Affero
+// General Public License. See LICENSE in project root for full license
+// information, or visit https://www.gnu.org/licenses/#AGPL
+//
+
+namespace Brutario.Core;
+
+using System;
+
+public class UndoElement
+{
+ public UndoElement(Action undo, Action redo)
+ {
+ Undo = undo;
+ Redo = redo;
+ }
+
+ public Action Undo
+ {
+ get;
+ }
+
+ public Action Redo
+ {
+ get;
+ }
+}
diff --git a/src/Brutario.Core/UndoEventArgs.cs b/src/Brutario.Core/UndoEventArgs.cs
new file mode 100644
index 0000000..29480a2
--- /dev/null
+++ b/src/Brutario.Core/UndoEventArgs.cs
@@ -0,0 +1,24 @@
+//
+// Copyright (c) 2022 spel werdz rite. All rights reserved. Licensed under GNU Affero
+// General Public License. See LICENSE in project root for full license
+// information, or visit https://www.gnu.org/licenses/#AGPL
+//
+
+namespace Brutario.Core;
+
+using System;
+
+public class UndoEventArgs : EventArgs
+{
+ public UndoEventArgs(UndoElement undoElement)
+ {
+ UndoElement = undoElement;
+ }
+
+ public UndoEventArgs(Action undo, Action redo)
+ : this(new UndoElement(undo, redo))
+ {
+ }
+
+ public UndoElement UndoElement { get; }
+}
diff --git a/src/Brutario.Core/UndoFactory.cs b/src/Brutario.Core/UndoFactory.cs
new file mode 100644
index 0000000..f0fe2e5
--- /dev/null
+++ b/src/Brutario.Core/UndoFactory.cs
@@ -0,0 +1,118 @@
+//
+// Copyright (c) 2022 spel werdz rite. All rights reserved. Licensed under GNU Affero
+// General Public License. See LICENSE in project root for full license
+// information, or visit https://www.gnu.org/licenses/#AGPL
+//
+
+namespace Brutario.Core;
+
+using System;
+using System.Collections.Generic;
+
+public class UndoFactory
+{
+ public UndoFactory()
+ {
+ UndoStack = new Stack();
+ RedoStack = new Stack();
+ }
+
+ public event EventHandler? Cleared;
+
+ public event EventHandler? UndoComplete;
+
+ public event EventHandler? RedoComplete;
+
+ public event EventHandler? UndoElementAdded;
+
+ public bool CanUndo
+ {
+ get
+ {
+ return UndoStack.Count > 0;
+ }
+ }
+
+ public bool CanRedo
+ {
+ get
+ {
+ return RedoStack.Count > 0;
+ }
+ }
+
+ private Stack UndoStack
+ {
+ get;
+ }
+
+ private Stack RedoStack
+ {
+ get;
+ }
+
+ public void Add(Action undo, Action redo)
+ {
+ Add(new UndoElement(undo, redo));
+ }
+
+ public void Add(UndoElement undoElement)
+ {
+ UndoStack.Push(undoElement);
+ RedoStack.Clear();
+ OnUndoElementAdded(new UndoEventArgs(undoElement));
+ }
+
+ public void Clear()
+ {
+ UndoStack.Clear();
+ RedoStack.Clear();
+ OnCleared(EventArgs.Empty);
+ }
+
+ public void Undo()
+ {
+ if (!CanUndo)
+ {
+ throw new InvalidOperationException();
+ }
+
+ var undoElement = UndoStack.Pop();
+ undoElement.Undo();
+ RedoStack.Push(undoElement);
+ OnUndoComplete(new UndoEventArgs(undoElement));
+ }
+
+ public void Redo()
+ {
+ if (!CanRedo)
+ {
+ throw new InvalidOperationException();
+ }
+
+ var undoElement = RedoStack.Pop();
+ undoElement.Redo();
+ UndoStack.Push(undoElement);
+ OnRedoComplete(new UndoEventArgs(undoElement));
+ }
+
+ protected virtual void OnCleared(EventArgs e)
+ {
+ Cleared?.Invoke(this, e);
+ }
+
+ protected virtual void OnUndoComplete(UndoEventArgs e)
+ {
+ UndoComplete?.Invoke(this, e);
+ }
+
+ protected virtual void OnRedoComplete(UndoEventArgs e)
+ {
+ RedoComplete?.Invoke(this, e);
+ }
+
+ protected virtual void OnUndoElementAdded(UndoEventArgs e)
+ {
+ UndoElementAdded?.Invoke(this, e);
+ }
+}
diff --git a/src/Brutario.Core/Views/IExceptionView.cs b/src/Brutario.Core/Views/IExceptionView.cs
new file mode 100644
index 0000000..9a0b91a
--- /dev/null
+++ b/src/Brutario.Core/Views/IExceptionView.cs
@@ -0,0 +1,14 @@
+namespace Brutario.Core;
+
+using System;
+
+public interface IExceptionView
+{
+ void Show(Exception ex);
+
+ void Show(string? message);
+
+ bool ShowAndPromptRetry(Exception ex);
+
+ bool ShowAndPromptRetry(string? message);
+}
diff --git a/src/Brutario.Core/Views/IMainView.cs b/src/Brutario.Core/Views/IMainView.cs
new file mode 100644
index 0000000..164e54e
--- /dev/null
+++ b/src/Brutario.Core/Views/IMainView.cs
@@ -0,0 +1,46 @@
+//
+// Copyright (c) 2022 spel werdz rite. All rights reserved. Licensed under GNU Affero
+// General Public License. See LICENSE in project root for full license
+// information, or visit https://www.gnu.org/licenses/#AGPL
+//
+
+namespace Brutario.Core;
+
+using System.Drawing;
+
+using Maseya.Smas.Smb1;
+
+public interface IMainView
+{
+ string Title { get; set; }
+
+ bool EditorEnabled { get; set; }
+
+ bool MapEditorEnabled { get; set; }
+
+ bool SaveEnabled { get; set; }
+
+ bool UndoEnabled { get; set; }
+
+ bool RedoEnabled { get; set; }
+
+ bool EditItemEnabled { get; set; }
+
+ bool PasteEnabled { get; set; }
+
+ bool DeleteAllEnabled { get; set; }
+
+ Player Player { get; set; }
+
+ PlayerState PlayerState { get; set; }
+
+ bool SpriteMode { get; set; }
+
+ int AreaNumber { get; set; }
+
+ int StartX { get; set; }
+
+ Size DrawAreaSize { get; }
+
+ void Redraw();
+}
diff --git a/src/Brutario.Core/Views/IObjectListView.cs b/src/Brutario.Core/Views/IObjectListView.cs
new file mode 100644
index 0000000..1663c10
--- /dev/null
+++ b/src/Brutario.Core/Views/IObjectListView.cs
@@ -0,0 +1,29 @@
+namespace Brutario.Core.Views;
+using System.Collections.Generic;
+
+using Maseya.Smas.Smb1.AreaData.ObjectData;
+
+public interface IObjectListView
+{
+ event EventHandler? AreaPlatformTypeChanged;
+
+ event EventHandler? SelectedIndexChanged;
+
+ event EventHandler? EditItem;
+
+ event EventHandler? AddItem_Click;
+
+ event EventHandler? DeleteItem_Click;
+
+ event EventHandler? ClearItems_Click;
+
+ event EventHandler? MoveItemDown_Click;
+
+ event EventHandler? MoveItemUp_Click;
+
+ AreaPlatformType AreaPlatformType { get; set; }
+
+ int SelectedIndex { get; set; }
+
+ IList Items { get; }
+}
diff --git a/src/Brutario.Win/App.config b/src/Brutario.Win/App.config
new file mode 100644
index 0000000..8381896
--- /dev/null
+++ b/src/Brutario.Win/App.config
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
+
+ True
+
+
+ 00:05:00
+
+
+ 04:00:00
+
+
+ False
+
+
+ True
+
+
+
+
\ No newline at end of file
diff --git a/src/Brutario.Win/AreaPixelRenderer.cs b/src/Brutario.Win/AreaPixelRenderer.cs
new file mode 100644
index 0000000..02dcb3c
--- /dev/null
+++ b/src/Brutario.Win/AreaPixelRenderer.cs
@@ -0,0 +1,367 @@
+namespace Brutario.Win;
+
+using System;
+using System.Collections.Generic;
+using System.Drawing;
+using System.Drawing.Drawing2D;
+using System.Drawing.Imaging;
+using System.Linq;
+using System.Threading.Tasks;
+
+using Core;
+
+using Maseya.Snes;
+
+public static class AreaPixelRenderer
+{
+ private delegate Color32BppArgb PixelTransform(
+ Color32BppArgb top,
+ Color32BppArgb bottom);
+
+ public static void DrawArea(Graphics graphics, in DrawData drawData)
+ {
+ DrawArea(
+ graphics,
+ drawData.BgColor,
+ drawData.Palette,
+ drawData.PixelData,
+ drawData.Bg1,
+ drawData.Sprites,
+ drawData.StartX,
+ drawData.Size,
+ drawData.Rectangles,
+ drawData.SelectedIndex,
+ drawData.SeparatorColor,
+ drawData.PassiveColor,
+ drawData.SelectColor);
+ }
+
+ public static void DrawArea(
+ Graphics graphics,
+ Color32BppArgb bgColor,
+ ReadOnlySpan palette,
+ ReadOnlySpan pixelData,
+ ReadOnlySpan bg1,
+ IEnumerable sprites,
+ int startX,
+ Size size,
+ IEnumerable rectangles,
+ int selectedIndex,
+ Color separatorColor,
+ Color passiveColor,
+ Color selectColor)
+ {
+ using var image = GetImage(
+ bgColor,
+ palette,
+ pixelData,
+ bg1,
+ sprites,
+ startX,
+ size);
+
+ using var imageGraphics = Graphics.FromImage(image);
+ DrawPageSeparators(imageGraphics, startX, image.Size, separatorColor);
+ DrawItemRectangle(
+ imageGraphics,
+ startX,
+ rectangles,
+ selectedIndex,
+ passiveColor,
+ selectColor);
+
+ graphics.InterpolationMode = InterpolationMode.NearestNeighbor;
+ graphics.SmoothingMode = SmoothingMode.None;
+ graphics.DrawImage(image, 0, 0, image.Width, image.Height);
+ }
+
+ public static void DrawItemRectangle(
+ Graphics graphics,
+ int startX,
+ IEnumerable rectangles,
+ int selectedIndex,
+ Color passiveColor,
+ Color selectColor)
+ {
+ using var selectBrush = new SolidBrush(Color.FromArgb(0x7F, selectColor));
+ using var passiveBrush = new SolidBrush(Color.FromArgb(0x7F, passiveColor));
+ var i = 0;
+ foreach (var rect in rectangles)
+ {
+ graphics.FillRectangle(
+ i == selectedIndex ? selectBrush : passiveBrush,
+ rect.X - (startX * 8),
+ rect.Y,
+ rect.Width,
+ rect.Height);
+ i++;
+ }
+ }
+
+ public static void DrawPageSeparators(
+ Graphics graphics,
+ int startX,
+ Size size,
+ Color color)
+ {
+ var offset = (startX * 8) & 0xFF;
+ using var pen = new Pen(color);
+ for (var pageX = 0xFF - offset; pageX < size.Width; pageX += 0x100)
+ {
+ graphics.DrawLine(pen, pageX, 0, pageX, size.Height);
+ }
+ }
+
+ public static Bitmap GetImage(
+ Color32BppArgb bgColor,
+ ReadOnlySpan palette,
+ ReadOnlySpan pixelData,
+ ReadOnlySpan bg1,
+ IEnumerable sprites,
+ int startX,
+ Size size)
+ {
+ var viewWidth = ((size.Width - 1) / 8) + 1;
+ var imageWidth = viewWidth * 8;
+ var pixels = RenderPixels(
+ bgColor,
+ palette,
+ pixelData,
+ bg1,
+ sprites,
+ startX,
+ size);
+
+ unsafe
+ {
+ fixed (Color32BppArgb* scan0 = pixels)
+ {
+ return new Bitmap(
+ imageWidth,
+ size.Height,
+ imageWidth * 4,
+ PixelFormat.Format32bppArgb,
+ (IntPtr)scan0);
+ }
+ }
+ }
+
+ public static Color32BppArgb[] RenderPixels(
+ Color32BppArgb bgColor,
+ ReadOnlySpan palette,
+ ReadOnlySpan pixelData,
+ ReadOnlySpan bg1,
+ IEnumerable sprites,
+ int startX,
+ Size size)
+ {
+ unsafe
+ {
+ fixed (Color32BppArgb* ptrPalette = palette)
+ fixed (byte* ptrPixelData = pixelData)
+ fixed (ObjTile* ptrBg1 = bg1)
+ {
+ // This is DEFINITELY bad practice. Spans are ref structs and should
+ // not be passed to asynchronous functions as they keep the value alive
+ // longer than it should be. However, the async methods are all
+ // finished within this function call, the pointers never leave scope.
+ // Ideally, these value should be allowed to be ref structs, but the
+ // language is limiting us, so we need to cheat a little right now.
+ var asyncPtrPalette = ptrPalette;
+ var asyncPtrPixelData = ptrPixelData;
+ var asyncPtrBg1 = ptrBg1;
+
+ var viewWidth = ((size.Width - 1) / 8) + 1;
+ var imageWidth = viewWidth * 8;
+
+ var result = new Color32BppArgb[imageWidth * size.Height];
+
+ sprites = sprites.Where(IsInFrame);
+
+ // Set the BG color to the area.
+ _ = Parallel.For(
+ fromInclusive: 0,
+ toExclusive: result.Length,
+ body: i => result[i] = bgColor);
+
+ _ = Parallel.ForEach(
+ source: sprites.Where(
+ sprite => HasLayer(sprite, LayerPriority.Priority0)),
+ body: sprite => RenderSprite(sprite));
+ _ = Parallel.For(
+ fromInclusive: 0,
+ toExclusive: size.Height / 8,
+ body: row => RenderRow(row, LayerPriority.Priority0));
+
+ _ = Parallel.ForEach(
+ source: sprites.Where(
+ sprite => HasLayer(sprite, LayerPriority.Priority2)),
+ body: sprite => RenderSprite(sprite));
+ _ = Parallel.For(
+ fromInclusive: 0,
+ toExclusive: size.Height / 8,
+ body: row => RenderRow(row, LayerPriority.Priority1));
+
+ _ = Parallel.ForEach(
+ source: sprites.Where(
+ sprite => HasLayer(sprite, LayerPriority.Priority3)),
+ body: sprite => RenderSprite(sprite));
+
+ _ = Parallel.ForEach(
+ source: sprites.Where(sprite => HasLayer(sprite, (LayerPriority)4)),
+ body: sprite => RenderSprite(sprite));
+
+ return result;
+
+ void RenderRow(int row, LayerPriority layerPriority)
+ {
+ var darken = row > 0x1D;
+ var rowIndex = Math.Min(row, 0x1C + (row & 1)) * 0x400;
+ var pixelRow = row * 8 * imageWidth;
+ for (var column = 0; column < viewWidth; column++, pixelRow += 8)
+ {
+ var tile8 = asyncPtrBg1[rowIndex + column + startX];
+ if (tile8.Priority != layerPriority)
+ {
+ continue;
+ }
+
+ var xFlip = tile8.XFlipped ? 7 : 0;
+ var yFlip = tile8.YFlipped ? 7 : 0;
+ var paletteIndex = tile8.PaletteIndex * 0x10;
+ var pixelIndex = tile8.TileIndex * 0x40;
+
+ for (var y = 0; y < 8; y++, pixelIndex += 8)
+ {
+ var index = pixelRow + ((y ^ yFlip) * imageWidth);
+ for (var x = 0; x < 8; x++, index++)
+ {
+ var pixel = asyncPtrPixelData[
+ pixelIndex + (x ^ xFlip)];
+
+ if (pixel != 0)
+ {
+ var color = asyncPtrPalette[paletteIndex + pixel];
+ if (darken)
+ {
+ color.R /= 2;
+ color.G /= 2;
+ color.B /= 2;
+ }
+
+ result[index] = color;
+ }
+ }
+ }
+ }
+ }
+
+ bool IsInFrame(Sprite sprite)
+ {
+ return (uint)(sprite.X - (startX * 8) + 8) <= (uint)imageWidth
+ && (uint)sprite.Y < (uint)(size.Height - 8);
+ }
+
+ bool HasLayer(Sprite sprite, LayerPriority layerPriority)
+ {
+ return (sprite.Tile.Priority - 2) / 3 == (int)layerPriority;
+ }
+
+ void RenderSprite(Sprite sprite)
+ {
+ var spriteX = sprite.X - (startX * 8);
+ var firstX = spriteX >= 0 ? 0 : -spriteX;
+
+ var tile8 = sprite.Tile;
+
+ var xFlip = tile8.FlipX ? 7 : 0;
+ var yFlip = tile8.FlipY ? 7 : 0;
+ var paletteIndex = tile8.PaletteIndex * 0x10;
+ var pixelIndex = tile8.TileIndex * 0x40;
+ var func = TransformPixel(sprite.TileProperties);
+ var pixelRow = (sprite.Y * imageWidth) + sprite.X - (startX * 8);
+
+ for (var y = 0; y < 8; y++, pixelIndex += 8)
+ {
+ var index = pixelRow + ((y ^ yFlip) * imageWidth);
+ for (var x = firstX; x < 8; x++, index++)
+ {
+ var pixel = asyncPtrPixelData[pixelIndex + (x ^ xFlip)];
+
+ if (pixel != 0)
+ {
+ result[index] = func(
+ asyncPtrPalette[paletteIndex + pixel],
+ result[index]);
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ private static PixelTransform TransformPixel(TileProperties tileProperties)
+ {
+ return tileProperties switch
+ {
+ TileProperties.Invert => (pixel, bottom) =>
+ {
+ pixel.R ^= 0xFF;
+ pixel.G ^= 0xFF;
+ pixel.B ^= 0xFF;
+ return pixel;
+ }
+ ,
+ TileProperties.Red => (pixel, bottom) =>
+ {
+ pixel.R = 0xFF;
+ return pixel;
+ }
+ ,
+ TileProperties.Green => (pixel, bottom) =>
+ {
+ pixel.G = 0xFF;
+ return pixel;
+ }
+ ,
+ TileProperties.Blue => (pixel, bottom) =>
+ {
+ pixel.B = 0xFF;
+ return pixel;
+ }
+ ,
+ TileProperties.Yellow => (pixel, bottom) =>
+ {
+ pixel.R = 0xFF;
+ pixel.G = 0xFF;
+ return pixel;
+ }
+
+ ,
+ TileProperties.Magenta => (pixel, bottom) =>
+ {
+ pixel.R = 0xFF;
+ pixel.B = 0xFF;
+ return pixel;
+ }
+ ,
+ TileProperties.Cyan => (pixel, bottom) =>
+ {
+ pixel.G = 0xFF;
+ pixel.B = 0xFF;
+ return pixel;
+ }
+ ,
+ TileProperties.Transparent => (pixel, bottom) =>
+ {
+ pixel.R = (byte)((pixel.R + (1 * bottom.R)) >> 1);
+ pixel.G = (byte)((pixel.G + (1 * bottom.G)) >> 1);
+ pixel.B = (byte)((pixel.B + (1 * bottom.B)) >> 1);
+ return pixel;
+ }
+ ,
+ _ => (pixel, bottom) => pixel,
+ };
+ }
+}
diff --git a/src/Brutario.Win/AutoSaveForm.Designer.cs b/src/Brutario.Win/AutoSaveForm.Designer.cs
new file mode 100644
index 0000000..ca506fa
--- /dev/null
+++ b/src/Brutario.Win/AutoSaveForm.Designer.cs
@@ -0,0 +1,201 @@
+namespace Brutario.Win;
+
+partial class AutoSaveForm
+{
+ ///
+ /// Required designer variable.
+ ///
+ private System.ComponentModel.IContainer components = null;
+
+ ///
+ /// Clean up any resources being used.
+ ///
+ /// true if managed resources should be disposed; otherwise, false.
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing && (components != null))
+ {
+ components.Dispose();
+ }
+ base.Dispose(disposing);
+ }
+
+ #region Windows Form Designer generated code
+
+ ///
+ /// Required method for Designer support - do not modify
+ /// the contents of this method with the code editor.
+ ///
+ private void InitializeComponent()
+ {
+ btnOK = new Button();
+ btnCancel = new Button();
+ chkAutoSave = new CheckBox();
+ tbxTime = new TextBox();
+ cbxUnits = new ComboBox();
+ chkPrune = new CheckBox();
+ tbxCutoffTime = new TextBox();
+ cbxCutoffUnits = new ComboBox();
+ gbxPruneMode = new GroupBox();
+ rdbProgressive = new RadioButton();
+ rdbConstant = new RadioButton();
+ gbxPruneMode.SuspendLayout();
+ SuspendLayout();
+ //
+ // btnOK
+ //
+ btnOK.Anchor = AnchorStyles.Bottom | AnchorStyles.Right;
+ btnOK.DialogResult = DialogResult.OK;
+ btnOK.Location = new Point(247, 180);
+ btnOK.Name = "btnOK";
+ btnOK.Size = new Size(94, 29);
+ btnOK.TabIndex = 0;
+ btnOK.Text = "&OK";
+ btnOK.UseVisualStyleBackColor = true;
+ //
+ // btnCancel
+ //
+ btnCancel.Anchor = AnchorStyles.Bottom | AnchorStyles.Right;
+ btnCancel.DialogResult = DialogResult.Cancel;
+ btnCancel.Location = new Point(347, 180);
+ btnCancel.Name = "btnCancel";
+ btnCancel.Size = new Size(94, 29);
+ btnCancel.TabIndex = 1;
+ btnCancel.Text = "&Cancel";
+ btnCancel.UseVisualStyleBackColor = true;
+ //
+ // chkAutoSave
+ //
+ chkAutoSave.AutoSize = true;
+ chkAutoSave.Checked = true;
+ chkAutoSave.CheckState = CheckState.Checked;
+ chkAutoSave.Location = new Point(12, 12);
+ chkAutoSave.Name = "chkAutoSave";
+ chkAutoSave.Size = new Size(135, 24);
+ chkAutoSave.TabIndex = 2;
+ chkAutoSave.Text = "Auto save every";
+ chkAutoSave.UseVisualStyleBackColor = true;
+ chkAutoSave.CheckedChanged += AutoSave_CheckedChanged;
+ //
+ // tbxTime
+ //
+ tbxTime.Location = new Point(153, 10);
+ tbxTime.Name = "tbxTime";
+ tbxTime.Size = new Size(73, 27);
+ tbxTime.TabIndex = 3;
+ tbxTime.TextChanged += Time_TextChanged;
+ //
+ // cbxUnits
+ //
+ cbxUnits.FormattingEnabled = true;
+ cbxUnits.Items.AddRange(new object[] { "seconds", "minutes", "hours" });
+ cbxUnits.Location = new Point(232, 10);
+ cbxUnits.Name = "cbxUnits";
+ cbxUnits.Size = new Size(151, 28);
+ cbxUnits.TabIndex = 4;
+ //
+ // chkPrune
+ //
+ chkPrune.AutoSize = true;
+ chkPrune.Checked = true;
+ chkPrune.CheckState = CheckState.Checked;
+ chkPrune.Location = new Point(12, 46);
+ chkPrune.Name = "chkPrune";
+ chkPrune.Size = new Size(222, 24);
+ chkPrune.TabIndex = 5;
+ chkPrune.Text = "Delete auto-saves older than";
+ chkPrune.UseVisualStyleBackColor = true;
+ chkPrune.CheckedChanged += Prune_CheckedChanged;
+ //
+ // tbxCutoffTime
+ //
+ tbxCutoffTime.Location = new Point(240, 44);
+ tbxCutoffTime.Name = "tbxCutoffTime";
+ tbxCutoffTime.Size = new Size(75, 27);
+ tbxCutoffTime.TabIndex = 6;
+ tbxCutoffTime.TextChanged += Time_TextChanged;
+ //
+ // cbxCutoffUnits
+ //
+ cbxCutoffUnits.FormattingEnabled = true;
+ cbxCutoffUnits.Items.AddRange(new object[] { "minutes", "hours", "days", "weeks" });
+ cbxCutoffUnits.Location = new Point(321, 44);
+ cbxCutoffUnits.Name = "cbxCutoffUnits";
+ cbxCutoffUnits.Size = new Size(116, 28);
+ cbxCutoffUnits.TabIndex = 7;
+ //
+ // gbxPruneMode
+ //
+ gbxPruneMode.Controls.Add(rdbProgressive);
+ gbxPruneMode.Controls.Add(rdbConstant);
+ gbxPruneMode.Location = new Point(12, 77);
+ gbxPruneMode.Name = "gbxPruneMode";
+ gbxPruneMode.Size = new Size(409, 91);
+ gbxPruneMode.TabIndex = 8;
+ gbxPruneMode.TabStop = false;
+ gbxPruneMode.Text = "Pruning Mode";
+ //
+ // rdbProgressive
+ //
+ rdbProgressive.AutoSize = true;
+ rdbProgressive.Location = new Point(6, 56);
+ rdbProgressive.Name = "rdbProgressive";
+ rdbProgressive.Size = new Size(388, 24);
+ rdbProgressive.TabIndex = 1;
+ rdbProgressive.TabStop = true;
+ rdbProgressive.Text = "Retain some saves but delete progressively older ones";
+ rdbProgressive.UseVisualStyleBackColor = true;
+ //
+ // rdbConstant
+ //
+ rdbConstant.AutoSize = true;
+ rdbConstant.Location = new Point(6, 26);
+ rdbConstant.Name = "rdbConstant";
+ rdbConstant.Size = new Size(282, 24);
+ rdbConstant.TabIndex = 0;
+ rdbConstant.TabStop = true;
+ rdbConstant.Text = "Delete all saves older than cutoff date";
+ rdbConstant.UseVisualStyleBackColor = true;
+ //
+ // AutoSaveForm
+ //
+ AcceptButton = btnOK;
+ CancelButton = btnCancel;
+ ClientSize = new Size(453, 221);
+ Controls.Add(gbxPruneMode);
+ Controls.Add(cbxCutoffUnits);
+ Controls.Add(tbxCutoffTime);
+ Controls.Add(chkPrune);
+ Controls.Add(cbxUnits);
+ Controls.Add(tbxTime);
+ Controls.Add(chkAutoSave);
+ Controls.Add(btnCancel);
+ Controls.Add(btnOK);
+ KeyPreview = true;
+ MaximizeBox = false;
+ MinimizeBox = false;
+ Name = "AutoSaveForm";
+ ShowIcon = false;
+ ShowInTaskbar = false;
+ Text = "Configure Auto Save";
+ Load += AutoSaveForm_Load;
+ gbxPruneMode.ResumeLayout(false);
+ gbxPruneMode.PerformLayout();
+ ResumeLayout(false);
+ PerformLayout();
+ }
+
+ #endregion
+
+ private System.Windows.Forms.Button btnOK;
+ private System.Windows.Forms.Button btnCancel;
+ private System.Windows.Forms.CheckBox chkAutoSave;
+ private System.Windows.Forms.TextBox tbxTime;
+ private System.Windows.Forms.ComboBox cbxUnits;
+ private System.Windows.Forms.CheckBox chkPrune;
+ private System.Windows.Forms.TextBox tbxCutoffTime;
+ private System.Windows.Forms.GroupBox gbxPruneMode;
+ private System.Windows.Forms.RadioButton rdbProgressive;
+ private System.Windows.Forms.RadioButton rdbConstant;
+ private System.Windows.Forms.ComboBox cbxCutoffUnits;
+}
\ No newline at end of file
diff --git a/src/Brutario.Win/AutoSaveForm.cs b/src/Brutario.Win/AutoSaveForm.cs
new file mode 100644
index 0000000..a3b8e26
--- /dev/null
+++ b/src/Brutario.Win/AutoSaveForm.cs
@@ -0,0 +1,202 @@
+namespace Brutario.Win;
+using System;
+using System.Diagnostics;
+using System.Globalization;
+using System.Windows.Forms;
+
+public partial class AutoSaveForm : Form
+{
+ public AutoSaveForm()
+ {
+ InitializeComponent();
+ }
+
+ public bool EnableAutoSave
+ {
+ get
+ {
+ return chkAutoSave.Checked;
+ }
+
+ set
+ {
+ chkAutoSave.Checked = value;
+ }
+ }
+
+ public bool EnablePruning
+ {
+ get
+ {
+ return chkPrune.Checked;
+ }
+
+ set
+ {
+ chkPrune.Checked = value;
+ }
+ }
+
+ public TimeSpan AutoSaveInterval
+ {
+ get
+ {
+ if (!Int32.TryParse(
+ tbxTime.Text,
+ CultureInfo.CurrentUICulture,
+ out var time))
+ {
+ return TimeSpan.Zero;
+ }
+
+ var conversion = (int)Math.Pow(60, cbxUnits.SelectedIndex);
+ return new TimeSpan(0, 0, time * conversion);
+ }
+
+ set
+ {
+ if (value.TotalMinutes < 1)
+ {
+ cbxUnits.SelectedIndex = 0;
+ tbxTime.Text = ((int)value.TotalSeconds)
+ .ToString(CultureInfo.CurrentUICulture);
+ }
+ else if (value.TotalHours < 1)
+ {
+ cbxUnits.SelectedIndex = 1;
+ tbxTime.Text += ((int)value.TotalMinutes)
+ .ToString(CultureInfo.CurrentUICulture);
+ }
+ else
+ {
+ cbxUnits.SelectedIndex = 2;
+ tbxTime.Text = ((int)value.TotalHours)
+ .ToString(CultureInfo.CurrentUICulture);
+ }
+ }
+ }
+
+ public TimeSpan PruningInterval
+ {
+ get
+ {
+ if (!Int32.TryParse(
+ tbxCutoffTime.Text,
+ CultureInfo.CurrentUICulture,
+ out var time))
+ {
+ return TimeSpan.Zero;
+ }
+
+ switch (cbxCutoffUnits.SelectedIndex)
+ {
+ case 0:
+ return new TimeSpan(0, time, 0);
+ case 1:
+ return new TimeSpan(time, 0, 0);
+ case 2:
+ return new TimeSpan(time, 0, 0, 0);
+ case 3:
+ return new TimeSpan(7 * time, 0, 0);
+ default:
+ Debug.Assert(false);
+ return TimeSpan.Zero;
+ }
+ }
+
+ set
+ {
+ if (value.TotalHours < 1)
+ {
+ cbxCutoffUnits.SelectedIndex = 0;
+ tbxCutoffTime.Text = ((int)value.TotalMinutes)
+ .ToString(CultureInfo.CurrentUICulture);
+ }
+ else if (value.TotalDays < 1)
+ {
+ cbxCutoffUnits.SelectedIndex = 1;
+ tbxCutoffTime.Text += ((int)value.TotalHours)
+ .ToString(CultureInfo.CurrentUICulture);
+ }
+ else if (value.TotalDays < 7)
+ {
+
+ cbxCutoffUnits.SelectedIndex = 2;
+ tbxCutoffTime.Text = ((int)value.TotalDays)
+ .ToString(CultureInfo.CurrentUICulture);
+ }
+ else
+ {
+ cbxCutoffUnits.SelectedIndex = 3;
+ tbxCutoffTime.Text = (7 * (int)value.TotalDays)
+ .ToString(CultureInfo.CurrentUICulture);
+ }
+ }
+ }
+
+ public bool HardCutoff
+ {
+ get
+ {
+ return rdbConstant.Checked;
+ }
+
+ set
+ {
+ rdbConstant.Checked = value;
+ rdbProgressive.Checked = !value;
+ }
+ }
+
+ private void UpdateOKEnabled()
+ {
+ if (EnableAutoSave)
+ {
+ if (!Int32.TryParse(
+ tbxTime.Text,
+ CultureInfo.CurrentUICulture,
+ out var _))
+ {
+ btnOK.Enabled = false;
+ return;
+ }
+ }
+
+ if (EnablePruning)
+ {
+ if (!Int32.TryParse(
+ tbxCutoffTime.Text,
+ CultureInfo.CurrentUICulture,
+ out var _))
+ {
+ btnOK.Enabled = false;
+ return;
+ }
+ }
+
+ btnOK.Enabled = true;
+ }
+
+ private void AutoSave_CheckedChanged(object sender, EventArgs e)
+ {
+ tbxTime.Enabled =
+ cbxUnits.Enabled = chkAutoSave.Checked;
+ UpdateOKEnabled();
+ }
+
+ private void Prune_CheckedChanged(object sender, EventArgs e)
+ {
+ tbxCutoffTime.Enabled =
+ cbxCutoffUnits.Enabled = chkPrune.Checked;
+ UpdateOKEnabled();
+ }
+
+ private void AutoSaveForm_Load(object sender, EventArgs e)
+ {
+ }
+
+ private void Time_TextChanged(object sender, EventArgs e)
+ {
+ UpdateOKEnabled();
+ }
+}
diff --git a/src/Brutario.Win/AutoSaveForm.resx b/src/Brutario.Win/AutoSaveForm.resx
new file mode 100644
index 0000000..8b2ff64
--- /dev/null
+++ b/src/Brutario.Win/AutoSaveForm.resx
@@ -0,0 +1,120 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
\ No newline at end of file
diff --git a/src/Brutario.Win/Brutario.Win.csproj b/src/Brutario.Win/Brutario.Win.csproj
new file mode 100644
index 0000000..0d28270
--- /dev/null
+++ b/src/Brutario.Win/Brutario.Win.csproj
@@ -0,0 +1,91 @@
+
+
+
+ WinExe
+ net8.0-windows
+ enable
+ true
+ enable
+ True
+ en-US
+
+
+
+
+
+
+
+
+
+
+ UserControl
+
+
+ Component
+
+
+ True
+ True
+ Settings.settings
+
+
+ Component
+
+
+
+
+
+ Form
+
+
+ Component
+
+
+ Form
+
+
+
+
+ Form
+
+
+
+ Form
+
+
+ True
+ True
+ Resources.resx
+
+
+
+ Form
+
+
+
+
+ Form
+
+
+ Component
+
+
+
+
+
+ Designer
+
+
+ ResXFileCodeGenerator
+ Resources.Designer.cs
+
+
+
+
+
+ SettingsSingleFileGenerator
+ Settings.Designer.cs
+
+
+
+
\ No newline at end of file
diff --git a/src/Brutario.Win/Controls/DesignControl.cs b/src/Brutario.Win/Controls/DesignControl.cs
new file mode 100644
index 0000000..337e7a0
--- /dev/null
+++ b/src/Brutario.Win/Controls/DesignControl.cs
@@ -0,0 +1,19 @@
+//
+// Copyright (c) 2022 spel werdz rite. All rights reserved. Licensed under GNU Affero
+// General Public License. See LICENSE in project root for full license
+// information, or visit https://www.gnu.org/licenses/#AGPL
+//
+
+namespace Brutario.Win.Controls;
+
+using System.Windows.Forms;
+
+public partial class DesignControl : UserControl
+{
+ public DesignControl()
+ {
+ DoubleBuffered = true;
+ ResizeRedraw = true;
+ BorderStyle = BorderStyle.FixedSingle;
+ }
+}
diff --git a/src/Brutario.Win/Controls/DialogProxy.cs b/src/Brutario.Win/Controls/DialogProxy.cs
new file mode 100644
index 0000000..a9a5be3
--- /dev/null
+++ b/src/Brutario.Win/Controls/DialogProxy.cs
@@ -0,0 +1,154 @@
+//
+// Copyright (c) 2022 spel werdz rite. All rights reserved. Licensed under GNU Affero
+// General Public License. See LICENSE in project root for full license
+// information, or visit https://www.gnu.org/licenses/#AGPL
+//
+
+namespace Brutario.Win.Controls;
+
+using System.ComponentModel;
+using System.Windows.Forms;
+
+///
+/// Provides a simplified representation of forms that are intended to only be handled
+/// as dialog windows. This is an abstract class.
+///
+///
+/// Many windows classes are intended to be used as modal dialog
+/// boxes. These classes usually do not intend to make public the many properties and
+/// methods that a form exposes. This class is therefore used to make public only the
+/// essential parameters that an application developer intends to make usable. The base
+/// is kept internal so inheritors can select which properties,
+/// methods, and events should be visible.
+///
+/// This class was inspired by the design of , , and .
+///
+[ToolboxItem(true)]
+[DesignTimeVisible(true)]
+public abstract class DialogProxy : Component
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ protected DialogProxy()
+ {
+ }
+
+ protected DialogProxy(IContainer container)
+ : this()
+ {
+ container.Add(this);
+ }
+
+ ///
+ /// Occurs when the user clicks the Help button in the dialog box.
+ ///
+ public event HelpEventHandler? HelpRequested;
+
+ ///
+ /// Gets or sets a value indicating whether the Help button is displayed in the
+ /// dialog box.
+ ///
+ public bool ShowHelp
+ {
+ get
+ {
+ return BaseForm.HelpButton;
+ }
+
+ set
+ {
+ BaseForm.HelpButton = value;
+ }
+ }
+
+ ///
+ /// Gets or sets the dialog box title.
+ ///
+ public string Title
+ {
+ get
+ {
+ return BaseForm.Text;
+ }
+
+ set
+ {
+ BaseForm.Text = value;
+ }
+ }
+
+ ///
+ /// Gets or sets an object that contains data about the control.
+ ///
+ [Localizable(false)]
+ [Bindable(true)]
+ [DefaultValue(null)]
+ [TypeConverter(typeof(StringConverter))]
+ public object Tag
+ {
+ get
+ {
+ return BaseForm.Tag;
+ }
+
+ set
+ {
+ BaseForm.Tag = value;
+ }
+ }
+
+ ///
+ /// Gets the to use for modal dialog operations.
+ ///
+ protected abstract Form BaseForm
+ {
+ get;
+ }
+
+ ///
+ /// Runs a common dialog box with a specified owner or default if none is given.
+ ///
+ ///
+ /// An that represents the top-level windows the will
+ /// own the modal dialog box, or to specify the currently
+ /// active window of your application.
+ ///
+ ///
+ /// The that this dialog box returns when it closes.
+ ///
+ public DialogResult ShowDialog(IWin32Window? owner = null)
+ {
+ return BaseForm.ShowDialog(owner);
+ }
+
+ ///
+ /// Raises the event.
+ ///
+ ///
+ /// A that contains the event data.
+ ///
+ protected virtual void OnHelpRequested(HelpEventArgs e)
+ {
+ HelpRequested?.Invoke(this, e);
+ }
+
+ ///
+ /// Releases the unmanaged resources used by the and
+ /// optionally releases the managed resources.
+ ///
+ ///
+ /// to released both managed and unmanaged resources; to release only unmanaged resources.
+ ///
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ BaseForm.Dispose();
+ }
+
+ base.Dispose(disposing);
+ }
+}
diff --git a/src/Brutario.Win/Controls/ListViewNF.cs b/src/Brutario.Win/Controls/ListViewNF.cs
new file mode 100644
index 0000000..2af9250
--- /dev/null
+++ b/src/Brutario.Win/Controls/ListViewNF.cs
@@ -0,0 +1,17 @@
+//
+// Copyright (c) 2022 spel werdz rite. All rights reserved. Licensed under GNU Affero
+// General Public License. See LICENSE in project root for full license
+// information, or visit https://www.gnu.org/licenses/#AGPL
+//
+
+namespace Brutario.Win.Controls;
+
+using System.Windows.Forms;
+
+public class ListViewNF : ListView
+{
+ public ListViewNF()
+ {
+ DoubleBuffered = true;
+ }
+}
diff --git a/src/Brutario.Win/Controls/RtlAwareMessageBox.cs b/src/Brutario.Win/Controls/RtlAwareMessageBox.cs
new file mode 100644
index 0000000..8400644
--- /dev/null
+++ b/src/Brutario.Win/Controls/RtlAwareMessageBox.cs
@@ -0,0 +1,357 @@
+//
+// Copyright (c) 2018 spel werdz rite. All rights reserved Licensed under GNU Affero
+// General Public License. See LICENSE in project root for full license
+// information, or visit https://www.gnu.org/licenses/#AGPL
+//
+
+/*
+Taken from:
+http://msdn.microsoft.com/query/dev12.query?appId=Dev12IDEF1&l=EN-US&k=k%28CA1300%29;k%28TargetFrameworkMoniker-.NETFramework
+*/
+
+namespace Brutario.Win.Controls;
+
+using System;
+using System.Globalization;
+using System.Windows.Forms;
+
+using static MessageBoxButtons;
+using static MessageBoxDefaultButton;
+using static MessageBoxIcon;
+using static MessageBoxOptions;
+
+public static class RtlAwareMessageBox
+{
+ public static DialogResult Show(string? text)
+ {
+ return Show(null, text, String.Empty);
+ }
+
+ public static DialogResult Show(IWin32Window? owner, string? text)
+ {
+ return Show(owner, text, String.Empty);
+ }
+
+ public static DialogResult Show(string? text, string? caption)
+ {
+ return Show(null, text, caption);
+ }
+
+ public static DialogResult Show(IWin32Window? owner, string? text, string? caption)
+ {
+ return Show(owner, text, caption, buttons: OK);
+ }
+
+ public static DialogResult Show(
+ string? text,
+ string? caption,
+ MessageBoxButtons buttons)
+ {
+ return Show(null, text, caption, buttons);
+ }
+
+ public static DialogResult Show(
+ IWin32Window? owner,
+ string? text,
+ string? caption,
+ MessageBoxButtons buttons)
+ {
+ return Show(owner, text, caption, buttons, icon: None);
+ }
+
+ public static DialogResult Show(
+ string? text,
+ string? caption,
+ MessageBoxButtons buttons,
+ MessageBoxIcon icon)
+ {
+ return Show(null, text, caption, buttons, icon);
+ }
+
+ public static DialogResult Show(
+ IWin32Window? owner,
+ string? text,
+ string? caption,
+ MessageBoxButtons buttons,
+ MessageBoxIcon icon)
+ {
+ return Show(
+ owner,
+ text,
+ caption,
+ buttons,
+ icon,
+ Button1);
+ }
+
+ public static DialogResult Show(
+ string? text,
+ string? caption,
+ MessageBoxButtons buttons,
+ MessageBoxIcon icon,
+ MessageBoxDefaultButton defaultButton)
+ {
+ return Show(null, text, caption, buttons, icon, defaultButton);
+ }
+
+ public static DialogResult Show(
+ IWin32Window? owner,
+ string? text,
+ string? caption,
+ MessageBoxButtons buttons,
+ MessageBoxIcon icon,
+ MessageBoxDefaultButton defaultButton)
+ {
+ return Show(owner, text, caption, buttons, icon, defaultButton, options: 0);
+ }
+
+ public static DialogResult Show(
+ string? text,
+ string? caption,
+ MessageBoxButtons buttons,
+ MessageBoxIcon icon,
+ MessageBoxDefaultButton defaultButton,
+ MessageBoxOptions options)
+ {
+ return Show(null, text, caption, buttons, icon, defaultButton, options);
+ }
+
+ public static DialogResult Show(
+ IWin32Window? owner,
+ string? text,
+ string? caption,
+ MessageBoxButtons buttons,
+ MessageBoxIcon icon,
+ MessageBoxDefaultButton defaultButton,
+ MessageBoxOptions options)
+ {
+ return MessageBox.Show(
+ owner,
+ text,
+ caption,
+ buttons,
+ icon,
+ defaultButton,
+ RightToLeftAwareOptions(owner, options));
+ }
+
+ public static DialogResult Show(
+ string? text,
+ string? caption,
+ MessageBoxButtons buttons,
+ MessageBoxIcon icon,
+ MessageBoxDefaultButton defaultButton,
+ MessageBoxOptions options,
+ bool displayHelpButton)
+ {
+ return MessageBox.Show(
+ text,
+ caption,
+ buttons,
+ icon,
+ defaultButton,
+ RightToLeftAwareOptions(null, options),
+ displayHelpButton);
+ }
+
+ public static DialogResult Show(
+ string? text,
+ string? caption,
+ MessageBoxButtons buttons,
+ MessageBoxIcon icon,
+ MessageBoxDefaultButton defaultButton,
+ MessageBoxOptions options,
+ string? helpFilePath)
+ {
+ return MessageBox.Show(
+ text,
+ caption,
+ buttons,
+ icon,
+ defaultButton,
+ RightToLeftAwareOptions(null, options),
+ helpFilePath);
+ }
+
+ public static DialogResult Show(
+ IWin32Window? owner,
+ string? text,
+ string? caption,
+ MessageBoxButtons buttons,
+ MessageBoxIcon icon,
+ MessageBoxDefaultButton defaultButton,
+ MessageBoxOptions options,
+ string? helpFilePath)
+ {
+ return MessageBox.Show(
+ owner,
+ text,
+ caption,
+ buttons,
+ icon,
+ defaultButton,
+ RightToLeftAwareOptions(owner, options),
+ helpFilePath);
+ }
+
+ public static DialogResult Show(
+ string? text,
+ string? caption,
+ MessageBoxButtons buttons,
+ MessageBoxIcon icon,
+ MessageBoxDefaultButton defaultButton,
+ MessageBoxOptions options,
+ string? helpFilePath,
+ string keyword)
+ {
+ return MessageBox.Show(
+ text,
+ caption,
+ buttons,
+ icon,
+ defaultButton,
+ RightToLeftAwareOptions(null, options),
+ helpFilePath,
+ keyword);
+ }
+
+ public static DialogResult Show(
+ IWin32Window? owner,
+ string? text,
+ string? caption,
+ MessageBoxButtons buttons,
+ MessageBoxIcon icon,
+ MessageBoxDefaultButton defaultButton,
+ MessageBoxOptions options,
+ string? helpFilePath,
+ string keyword)
+ {
+ return MessageBox.Show(
+ owner,
+ text,
+ caption,
+ buttons,
+ icon,
+ defaultButton,
+ RightToLeftAwareOptions(owner, options),
+ helpFilePath,
+ keyword);
+ }
+
+ public static DialogResult Show(
+ string? text,
+ string? caption,
+ MessageBoxButtons buttons,
+ MessageBoxIcon icon,
+ MessageBoxDefaultButton defaultButton,
+ MessageBoxOptions options,
+ string? helpFilePath,
+ HelpNavigator navigator)
+ {
+ return MessageBox.Show(
+ text,
+ caption,
+ buttons,
+ icon,
+ defaultButton,
+ RightToLeftAwareOptions(null, options),
+ helpFilePath,
+ navigator);
+ }
+
+ public static DialogResult Show(
+ IWin32Window? owner,
+ string? text,
+ string? caption,
+ MessageBoxButtons buttons,
+ MessageBoxIcon icon,
+ MessageBoxDefaultButton defaultButton,
+ MessageBoxOptions options,
+ string? helpFilePath,
+ HelpNavigator navigator)
+ {
+ return MessageBox.Show(
+ owner,
+ text,
+ caption,
+ buttons,
+ icon,
+ defaultButton,
+ RightToLeftAwareOptions(owner, options),
+ helpFilePath,
+ navigator);
+ }
+
+ public static DialogResult Show(
+ string? text,
+ string? caption,
+ MessageBoxButtons buttons,
+ MessageBoxIcon icon,
+ MessageBoxDefaultButton defaultButton,
+ MessageBoxOptions options,
+ string? helpFilePath,
+ HelpNavigator navigator,
+ object param)
+ {
+ return MessageBox.Show(
+ text,
+ caption,
+ buttons,
+ icon,
+ defaultButton,
+ RightToLeftAwareOptions(null, options),
+ helpFilePath,
+ navigator,
+ param);
+ }
+
+ public static DialogResult Show(
+ IWin32Window? owner,
+ string? text,
+ string? caption,
+ MessageBoxButtons buttons,
+ MessageBoxIcon icon,
+ MessageBoxDefaultButton defaultButton,
+ MessageBoxOptions options,
+ string? helpFilePath,
+ HelpNavigator navigator,
+ object param)
+ {
+ return MessageBox.Show(
+ owner,
+ text,
+ caption,
+ buttons,
+ icon,
+ defaultButton,
+ RightToLeftAwareOptions(owner, options),
+ helpFilePath,
+ navigator,
+ param);
+ }
+
+ public static MessageBoxOptions RightToLeftAwareOptions(
+ IWin32Window? owner,
+ MessageBoxOptions options)
+ {
+ if (IsRightToLeft(owner))
+ {
+ options |= RtlReading;
+ options |= RightAlign;
+ }
+
+ return options;
+ }
+
+ public static bool IsRightToLeft(IWin32Window? owner)
+ {
+ var current = CultureInfo.CurrentUICulture.TextInfo.IsRightToLeft;
+ if (owner is null)
+ {
+ return current;
+ }
+
+ var control = Control.FromHandle(owner.Handle);
+ return control is null ? current : control.RightToLeft == RightToLeft.Yes;
+ }
+}
diff --git a/src/Brutario.Win/Controls/ToolStripRadioButtonMenuItem.cs b/src/Brutario.Win/Controls/ToolStripRadioButtonMenuItem.cs
new file mode 100644
index 0000000..5f053fd
--- /dev/null
+++ b/src/Brutario.Win/Controls/ToolStripRadioButtonMenuItem.cs
@@ -0,0 +1,200 @@
+//
+// Copyright (c) 2022 spel werdz rite. All rights reserved. Licensed under GNU Affero
+// General Public License. See LICENSE in project root for full license
+// information, or visit https://www.gnu.org/licenses/#AGPL
+//
+
+namespace Brutario.Win.Controls;
+
+using System;
+using System.Drawing;
+using System.Windows.Forms;
+using System.Windows.Forms.VisualStyles;
+
+public class ToolStripRadioButtonMenuItem : ToolStripMenuItem
+{
+ public ToolStripRadioButtonMenuItem()
+ : base()
+ {
+ CheckOnClick = true;
+ }
+
+ // Enable the item only if its parent item is in the checked state and its Enabled
+ // property has not been explicitly set to false.
+ public override bool Enabled
+ {
+ get
+ {
+ // Use the base value in design mode to prevent the designer from setting
+ // the base value to the calculated value.
+ return base.Enabled && (
+ DesignMode ||
+ OwnerItem is not ToolStripMenuItem ownerMenuItem ||
+ !ownerMenuItem.CheckOnClick ||
+ ownerMenuItem.CheckOnClick);
+ }
+
+ set
+ {
+ base.Enabled = value;
+ }
+ }
+
+ private bool MouseHoverState { get; set; }
+
+ private bool MouseDownState { get; set; }
+
+ protected override void OnCheckedChanged(EventArgs e)
+ {
+ base.OnCheckedChanged(e);
+
+ // If this item is no longer in the checked state or if its parent has not yet
+ // been initialized, do nothing.
+ if (!Checked || Parent == null)
+ {
+ return;
+ }
+
+ // Clear the checked state for all siblings.
+ foreach (ToolStripItem item in Parent.Items)
+ {
+ if (item is ToolStripRadioButtonMenuItem radioItem
+ && radioItem != this
+ && radioItem.Checked)
+ {
+ radioItem.Checked = false;
+
+ // Only one item can be selected at a time, so there is no need to continue.
+ return;
+ }
+ }
+ }
+
+ protected override void OnClick(EventArgs e)
+ {
+ // If the item is already in the checked state, do not call the base method,
+ // which would toggle the value.
+ if (Checked)
+ {
+ return;
+ }
+
+ base.OnClick(e);
+ }
+
+ // Let the item paint itself, and then paint the RadioButton where the check mark
+ // is normally displayed.
+ protected override void OnPaint(PaintEventArgs e)
+ {
+ if (Image is null)
+ {
+ // If the client sets the Image property, the selection behavior remains
+ // unchanged, but the RadioButton is not displayed and the selection is
+ // indicated only by the selection rectangle.
+ base.OnPaint(e);
+ return;
+ }
+ else
+ {
+ // If the Image property is not set, call the base OnPaint method with the
+ // CheckState property temporarily cleared to prevent the check mark from
+ // being painted.
+ var currentState = CheckState;
+ CheckState = CheckState.Unchecked;
+ base.OnPaint(e);
+ CheckState = currentState;
+ }
+
+ // Determine the correct state of the RadioButton.
+ var buttonState = RadioButtonState.UncheckedNormal;
+ if (Enabled)
+ {
+ if (MouseDownState)
+ {
+ buttonState = Checked
+ ? RadioButtonState.CheckedPressed
+ : RadioButtonState.UncheckedPressed;
+ }
+ else if (MouseHoverState)
+ {
+ buttonState = Checked
+ ? RadioButtonState.CheckedHot
+ : RadioButtonState.UncheckedHot;
+ }
+ else if (Checked)
+ {
+ buttonState = RadioButtonState.CheckedNormal;
+ }
+ }
+ else
+ {
+ buttonState = Checked
+ ? RadioButtonState.CheckedDisabled
+ : RadioButtonState.UncheckedDisabled;
+ }
+
+ // Calculate the position at which to display the RadioButton.
+ var offset = (ContentRectangle.Height -
+ RadioButtonRenderer.GetGlyphSize(
+ e.Graphics, buttonState).Height) / 2;
+ var imageLocation = new Point(
+ ContentRectangle.Location.X + 4,
+ ContentRectangle.Location.Y + offset);
+
+ // Paint the RadioButton.
+ RadioButtonRenderer.DrawRadioButton(e.Graphics, imageLocation, buttonState);
+ }
+
+ protected override void OnMouseEnter(EventArgs e)
+ {
+ MouseHoverState = true;
+
+ // Force the item to repaint with the new RadioButton state.
+ Invalidate();
+
+ base.OnMouseEnter(e);
+ }
+
+ protected override void OnMouseLeave(EventArgs e)
+ {
+ MouseHoverState = false;
+ base.OnMouseLeave(e);
+ }
+
+ protected override void OnMouseDown(MouseEventArgs e)
+ {
+ MouseDownState = true;
+
+ // Force the item to repaint with the new RadioButton state.
+ Invalidate();
+
+ base.OnMouseDown(e);
+ }
+
+ protected override void OnMouseUp(MouseEventArgs e)
+ {
+ MouseDownState = false;
+ base.OnMouseUp(e);
+ }
+
+ // When OwnerItem becomes available, if it is a ToolStripMenuItem with a
+ // CheckOnClick property value of true, subscribe to its CheckedChanged event.
+ protected override void OnOwnerChanged(EventArgs e)
+ {
+ if (OwnerItem is ToolStripMenuItem ownerMenuItem
+ && ownerMenuItem.CheckOnClick)
+ {
+ ownerMenuItem.CheckedChanged +=
+ new EventHandler(OwnerMenuItem_CheckedChanged);
+ }
+
+ base.OnOwnerChanged(e);
+ }
+
+ // When the checked state of the parent item changes, repaint the item so that the
+ // new Enabled state is displayed.
+ private void OwnerMenuItem_CheckedChanged(object? sender, EventArgs e)
+ {
+ Invalidate();
+ }
+}
diff --git a/src/Brutario.Win/Dialogs/BaseForms/HeaderEditorForm.Designer.cs b/src/Brutario.Win/Dialogs/BaseForms/HeaderEditorForm.Designer.cs
new file mode 100644
index 0000000..0cc1b7c
--- /dev/null
+++ b/src/Brutario.Win/Dialogs/BaseForms/HeaderEditorForm.Designer.cs
@@ -0,0 +1,287 @@
+//
+// Copyright (c) 2022 spel werdz rite. All rights reserved. Licensed
+// under GNU Affero General Public License. See LICENSE in project
+// root for full license information, or visit
+// https://www.gnu.org/licenses/#AGPL
+//
+
+namespace Brutario.Win.Dialogs.BaseForms
+{
+ partial class HeaderEditorForm
+ {
+ ///
+ /// Required designer variable.
+ ///
+ private System.ComponentModel.IContainer components = null;
+
+ ///
+ /// Clean up any resources being used.
+ ///
+ /// true if managed resources should be disposed; otherwise, false.
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing && (components != null))
+ {
+ components.Dispose();
+ }
+ base.Dispose(disposing);
+ }
+
+ #region Windows Form Designer generated code
+
+ ///
+ /// Required method for Designer support - do not modify
+ /// the contents of this method with the code editor.
+ ///
+ private void InitializeComponent()
+ {
+ this.lblTime = new System.Windows.Forms.Label();
+ this.lblPosition = new System.Windows.Forms.Label();
+ this.lblScenery = new System.Windows.Forms.Label();
+ this.cbxTime = new System.Windows.Forms.ComboBox();
+ this.cbxPosition = new System.Windows.Forms.ComboBox();
+ this.cbxForeground = new System.Windows.Forms.ComboBox();
+ this.lblForeground = new System.Windows.Forms.Label();
+ this.cbxAreaPlatformType = new System.Windows.Forms.ComboBox();
+ this.lblAreaPlatformType = new System.Windows.Forms.Label();
+ this.cbxBackgroundScenery = new System.Windows.Forms.ComboBox();
+ this.lblTerrainMode = new System.Windows.Forms.Label();
+ this.cbxTerrainMode = new System.Windows.Forms.ComboBox();
+ this.btnOK = new System.Windows.Forms.Button();
+ this.btnCancel = new System.Windows.Forms.Button();
+ this.SuspendLayout();
+ //
+ // lblTime
+ //
+ this.lblTime.AutoSize = true;
+ this.lblTime.Location = new System.Drawing.Point(12, 15);
+ this.lblTime.Name = "lblTime";
+ this.lblTime.Size = new System.Drawing.Size(30, 13);
+ this.lblTime.TabIndex = 0;
+ this.lblTime.Text = "Time";
+ //
+ // lblPosition
+ //
+ this.lblPosition.AutoSize = true;
+ this.lblPosition.Location = new System.Drawing.Point(12, 42);
+ this.lblPosition.Name = "lblPosition";
+ this.lblPosition.Size = new System.Drawing.Size(44, 13);
+ this.lblPosition.TabIndex = 1;
+ this.lblPosition.Text = "Position";
+ //
+ // lblScenery
+ //
+ this.lblScenery.AutoSize = true;
+ this.lblScenery.Location = new System.Drawing.Point(12, 124);
+ this.lblScenery.Name = "lblScenery";
+ this.lblScenery.Size = new System.Drawing.Size(46, 13);
+ this.lblScenery.TabIndex = 4;
+ this.lblScenery.Text = "Scenery";
+ //
+ // cbxTime
+ //
+ this.cbxTime.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
+ this.cbxTime.FormattingEnabled = true;
+ this.cbxTime.Items.AddRange(new object[] {
+ "Not Set",
+ "400",
+ "300",
+ "200"});
+ this.cbxTime.Location = new System.Drawing.Point(115, 12);
+ this.cbxTime.Name = "cbxTime";
+ this.cbxTime.Size = new System.Drawing.Size(245, 21);
+ this.cbxTime.TabIndex = 7;
+ this.cbxTime.SelectedIndexChanged += new System.EventHandler(this.Value_SelectedIndexChanged);
+ //
+ // cbxPosition
+ //
+ this.cbxPosition.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
+ this.cbxPosition.FormattingEnabled = true;
+ this.cbxPosition.Items.AddRange(new object[] {
+ "-1",
+ "-1; from another area",
+ "10",
+ "4",
+ "-1",
+ "-1",
+ "10 (Autowalk)",
+ "10 (Autowalk)"});
+ this.cbxPosition.Location = new System.Drawing.Point(115, 39);
+ this.cbxPosition.Name = "cbxPosition";
+ this.cbxPosition.Size = new System.Drawing.Size(245, 21);
+ this.cbxPosition.TabIndex = 8;
+ this.cbxPosition.SelectedIndexChanged += new System.EventHandler(this.Value_SelectedIndexChanged);
+ //
+ // cbxForeground
+ //
+ this.cbxForeground.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
+ this.cbxForeground.FormattingEnabled = true;
+ this.cbxForeground.Items.AddRange(new object[] {
+ "None",
+ "Underwater",
+ "Castle Wall (Unused)",
+ "Over Water",
+ "Night (Unused)",
+ "Snow (Unused)",
+ "Night and Snow (Unused)",
+ "Castle (unused)"});
+ this.cbxForeground.Location = new System.Drawing.Point(115, 66);
+ this.cbxForeground.Name = "cbxForeground";
+ this.cbxForeground.Size = new System.Drawing.Size(245, 21);
+ this.cbxForeground.TabIndex = 9;
+ this.cbxForeground.SelectedIndexChanged += new System.EventHandler(this.Value_SelectedIndexChanged);
+ //
+ // lblForeground
+ //
+ this.lblForeground.AutoSize = true;
+ this.lblForeground.Location = new System.Drawing.Point(12, 69);
+ this.lblForeground.Name = "lblForeground";
+ this.lblForeground.Size = new System.Drawing.Size(61, 13);
+ this.lblForeground.TabIndex = 10;
+ this.lblForeground.Text = "Foreground";
+ //
+ // cbxAreaPlatformType
+ //
+ this.cbxAreaPlatformType.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
+ this.cbxAreaPlatformType.FormattingEnabled = true;
+ this.cbxAreaPlatformType.Items.AddRange(new object[] {
+ "Trees",
+ "Mushrooms",
+ "Bullet Bill Turrets",
+ "Cloud Ground"});
+ this.cbxAreaPlatformType.Location = new System.Drawing.Point(115, 93);
+ this.cbxAreaPlatformType.Name = "cbxAreaPlatformType";
+ this.cbxAreaPlatformType.Size = new System.Drawing.Size(245, 21);
+ this.cbxAreaPlatformType.TabIndex = 11;
+ this.cbxAreaPlatformType.SelectedIndexChanged += new System.EventHandler(this.Value_SelectedIndexChanged);
+ //
+ // lblAreaPlatformType
+ //
+ this.lblAreaPlatformType.AutoSize = true;
+ this.lblAreaPlatformType.Location = new System.Drawing.Point(12, 96);
+ this.lblAreaPlatformType.Name = "lblAreaPlatformType";
+ this.lblAreaPlatformType.Size = new System.Drawing.Size(97, 13);
+ this.lblAreaPlatformType.TabIndex = 12;
+ this.lblAreaPlatformType.Text = "Area Platform Type";
+ //
+ // cbxBackgroundScenery
+ //
+ this.cbxBackgroundScenery.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
+ this.cbxBackgroundScenery.FormattingEnabled = true;
+ this.cbxBackgroundScenery.Items.AddRange(new object[] {
+ "Nothing",
+ "Clouds",
+ "Mountain",
+ "Fence"});
+ this.cbxBackgroundScenery.Location = new System.Drawing.Point(115, 121);
+ this.cbxBackgroundScenery.Name = "cbxBackgroundScenery";
+ this.cbxBackgroundScenery.Size = new System.Drawing.Size(245, 21);
+ this.cbxBackgroundScenery.TabIndex = 13;
+ this.cbxBackgroundScenery.SelectedIndexChanged += new System.EventHandler(this.Value_SelectedIndexChanged);
+ //
+ // lblTerrainMode
+ //
+ this.lblTerrainMode.AutoSize = true;
+ this.lblTerrainMode.Location = new System.Drawing.Point(12, 152);
+ this.lblTerrainMode.Name = "lblTerrainMode";
+ this.lblTerrainMode.Size = new System.Drawing.Size(70, 13);
+ this.lblTerrainMode.TabIndex = 14;
+ this.lblTerrainMode.Text = "Terrain Mode";
+ //
+ // cbxTerrainMode
+ //
+ this.cbxTerrainMode.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
+ this.cbxTerrainMode.FormattingEnabled = true;
+ this.cbxTerrainMode.Items.AddRange(new object[] {
+ "None",
+ "2-tile-high floor with no ceiling",
+ "2-tile-high floor with 1-tile-high ceiling",
+ "2-tile-high floor with 3-tile-high ceiling",
+ "2-tile-high floor with 4-tile-high ceiling",
+ "2-tile-high floor with 8-tile-high ceiling",
+ "5-tile-high floor with 1-tile-high ceiling",
+ "5-tile-high floor with 3-tile-high ceiling",
+ "5-tile-high floor with 4-tile-high ceiling",
+ "6-tile-high floor with 1-tile-high ceiling",
+ "No floor with 1-tile-high ceiling",
+ "6-tile-high floor with 4-tile-high ceiling",
+ "9-tile-high floor with 1-tile-high ceiling",
+ "2-tile-high floor with 1-tile-high ceiling and 5 tiles in the middle",
+ "2-tile-high floor with 1-tile-high ceiling and 4 tiles in the middle",
+ "Floor tiles everywhere"});
+ this.cbxTerrainMode.Location = new System.Drawing.Point(115, 148);
+ this.cbxTerrainMode.Name = "cbxTerrainMode";
+ this.cbxTerrainMode.Size = new System.Drawing.Size(245, 21);
+ this.cbxTerrainMode.TabIndex = 15;
+ this.cbxTerrainMode.SelectedIndexChanged += new System.EventHandler(this.Value_SelectedIndexChanged);
+ //
+ // btnOK
+ //
+ this.btnOK.DialogResult = System.Windows.Forms.DialogResult.OK;
+ this.btnOK.Location = new System.Drawing.Point(204, 174);
+ this.btnOK.Name = "btnOK";
+ this.btnOK.Size = new System.Drawing.Size(75, 23);
+ this.btnOK.TabIndex = 16;
+ this.btnOK.Text = "&OK";
+ this.btnOK.UseVisualStyleBackColor = true;
+ //
+ // btnCancel
+ //
+ this.btnCancel.DialogResult = System.Windows.Forms.DialogResult.Cancel;
+ this.btnCancel.Location = new System.Drawing.Point(285, 175);
+ this.btnCancel.Name = "btnCancel";
+ this.btnCancel.Size = new System.Drawing.Size(75, 23);
+ this.btnCancel.TabIndex = 17;
+ this.btnCancel.Text = "&Cancel";
+ this.btnCancel.UseVisualStyleBackColor = true;
+ //
+ // HeaderEditorForm
+ //
+ this.AcceptButton = this.btnOK;
+ this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
+ this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
+ this.CancelButton = this.btnCancel;
+ this.ClientSize = new System.Drawing.Size(372, 210);
+ this.Controls.Add(this.btnCancel);
+ this.Controls.Add(this.btnOK);
+ this.Controls.Add(this.cbxTerrainMode);
+ this.Controls.Add(this.lblTerrainMode);
+ this.Controls.Add(this.cbxBackgroundScenery);
+ this.Controls.Add(this.lblAreaPlatformType);
+ this.Controls.Add(this.cbxAreaPlatformType);
+ this.Controls.Add(this.lblForeground);
+ this.Controls.Add(this.cbxForeground);
+ this.Controls.Add(this.cbxPosition);
+ this.Controls.Add(this.cbxTime);
+ this.Controls.Add(this.lblScenery);
+ this.Controls.Add(this.lblPosition);
+ this.Controls.Add(this.lblTime);
+ this.MaximizeBox = false;
+ this.MinimizeBox = false;
+ this.Name = "HeaderEditorForm";
+ this.ShowIcon = false;
+ this.ShowInTaskbar = false;
+ this.Text = "Edit Header";
+ this.ResumeLayout(false);
+ this.PerformLayout();
+
+ }
+
+ #endregion
+
+ private System.Windows.Forms.Label lblTime;
+ private System.Windows.Forms.Label lblPosition;
+ private System.Windows.Forms.Label lblScenery;
+ private System.Windows.Forms.ComboBox cbxTime;
+ private System.Windows.Forms.ComboBox cbxPosition;
+ private System.Windows.Forms.ComboBox cbxForeground;
+ private System.Windows.Forms.Label lblForeground;
+ private System.Windows.Forms.ComboBox cbxAreaPlatformType;
+ private System.Windows.Forms.Label lblAreaPlatformType;
+ private System.Windows.Forms.ComboBox cbxBackgroundScenery;
+ private System.Windows.Forms.Label lblTerrainMode;
+ private System.Windows.Forms.ComboBox cbxTerrainMode;
+ private System.Windows.Forms.Button btnOK;
+ private System.Windows.Forms.Button btnCancel;
+ }
+}
diff --git a/src/Brutario.Win/Dialogs/BaseForms/HeaderEditorForm.cs b/src/Brutario.Win/Dialogs/BaseForms/HeaderEditorForm.cs
new file mode 100644
index 0000000..4eccbfd
--- /dev/null
+++ b/src/Brutario.Win/Dialogs/BaseForms/HeaderEditorForm.cs
@@ -0,0 +1,132 @@
+//
+// Copyright (c) 2022 spel werdz rite. All rights reserved. Licensed under GNU Affero
+// General Public License. See LICENSE in project root for full license
+// information, or visit https://www.gnu.org/licenses/#AGPL
+//
+
+namespace Brutario.Win.Dialogs.BaseForms;
+
+using System;
+using System.Windows.Forms;
+
+using Maseya.Smas.Smb1.AreaData.HeaderData;
+using Maseya.Smas.Smb1.AreaData.ObjectData;
+
+internal partial class HeaderEditorForm : Form
+{
+ public HeaderEditorForm()
+ {
+ InitializeComponent();
+
+ AreaHeader = default;
+ }
+
+ public event EventHandler? AreaHeaderChanged;
+
+ public StartTime StartTime
+ {
+ get
+ {
+ return (StartTime)cbxTime.SelectedIndex;
+ }
+
+ set
+ {
+ cbxTime.SelectedIndex = (int)value;
+ }
+ }
+
+ public StartYPosition StartYPosition
+ {
+ get
+ {
+ return (StartYPosition)cbxPosition.SelectedIndex;
+ }
+
+ set
+ {
+ cbxPosition.SelectedIndex = (int)value;
+ }
+ }
+
+ public ForegroundScenery ForegroundScenery
+ {
+ get
+ {
+ return (ForegroundScenery)cbxForeground.SelectedIndex;
+ }
+
+ set
+ {
+ cbxForeground.SelectedIndex = (int)value;
+ }
+ }
+
+ public AreaPlatformType AreaPlatformType
+ {
+ get
+ {
+ return (AreaPlatformType)cbxAreaPlatformType.SelectedIndex;
+ }
+
+ set
+ {
+ cbxAreaPlatformType.SelectedIndex = (int)value;
+ }
+ }
+
+ public BackgroundScenery BackgroundScenery
+ {
+ get
+ {
+ return (BackgroundScenery)cbxBackgroundScenery.SelectedIndex;
+ }
+
+ set
+ {
+ cbxBackgroundScenery.SelectedIndex = (int)value;
+ }
+ }
+
+ public TerrainMode TerrainMode
+ {
+ get
+ {
+ return (TerrainMode)cbxTerrainMode.SelectedIndex;
+ }
+
+ set
+ {
+ cbxTerrainMode.SelectedIndex = (int)value;
+ }
+ }
+
+ public AreaHeader AreaHeader
+ {
+ get
+ {
+ return new AreaHeader(
+ StartTime,
+ StartYPosition,
+ ForegroundScenery,
+ AreaPlatformType,
+ BackgroundScenery,
+ TerrainMode);
+ }
+
+ set
+ {
+ StartTime = value.StartTime;
+ StartYPosition = value.StartYPosition;
+ ForegroundScenery = value.ForegroundScenery;
+ AreaPlatformType = value.AreaPlatformType;
+ BackgroundScenery = value.BackgroundScenery;
+ TerrainMode = value.TerrainMode;
+ }
+ }
+
+ private void Value_SelectedIndexChanged(object? sender, EventArgs e)
+ {
+ AreaHeaderChanged?.Invoke(this, EventArgs.Empty);
+ }
+}
diff --git a/src/Brutario.Win/Dialogs/BaseForms/HeaderEditorForm.resx b/src/Brutario.Win/Dialogs/BaseForms/HeaderEditorForm.resx
new file mode 100644
index 0000000..1af7de1
--- /dev/null
+++ b/src/Brutario.Win/Dialogs/BaseForms/HeaderEditorForm.resx
@@ -0,0 +1,120 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
\ No newline at end of file
diff --git a/src/Brutario.Win/Dialogs/BaseForms/ObjectEditorForm.Designer.cs b/src/Brutario.Win/Dialogs/BaseForms/ObjectEditorForm.Designer.cs
new file mode 100644
index 0000000..f6bd2f4
--- /dev/null
+++ b/src/Brutario.Win/Dialogs/BaseForms/ObjectEditorForm.Designer.cs
@@ -0,0 +1,422 @@
+//
+// Copyright (c) 2022 spel werdz rite. All rights reserved. Licensed
+// under GNU Affero General Public License. See LICENSE in project
+// root for full license information, or visit
+// https://www.gnu.org/licenses/#AGPL
+//
+
+namespace Brutario.Win.Dialogs.BaseForms
+{
+ partial class ObjectEditorForm
+ {
+ ///
+ /// Required designer variable.
+ ///
+ private System.ComponentModel.IContainer components = null;
+
+ ///
+ /// Clean up any resources being used.
+ ///
+ /// true if managed resources should be disposed; otherwise, false.
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing && (components != null))
+ {
+ components.Dispose();
+ }
+ base.Dispose(disposing);
+ }
+
+ #region Windows Form Designer generated code
+
+ ///
+ /// Required method for Designer support - do not modify
+ /// the contents of this method with the code editor.
+ ///
+ private void InitializeComponent()
+ {
+ this.btnOK = new System.Windows.Forms.Button();
+ this.btnCancel = new System.Windows.Forms.Button();
+ this.groupBox1 = new System.Windows.Forms.GroupBox();
+ this.lblForegroundScenery = new System.Windows.Forms.Label();
+ this.cbxBackgroundScenery = new System.Windows.Forms.ComboBox();
+ this.cbxForegroundScenery = new System.Windows.Forms.ComboBox();
+ this.lblBackgroundScenery = new System.Windows.Forms.Label();
+ this.cbxTerrainMode = new System.Windows.Forms.ComboBox();
+ this.lblTerrainMode = new System.Windows.Forms.Label();
+ this.nudLength = new System.Windows.Forms.NumericUpDown();
+ this.lblLength = new System.Windows.Forms.Label();
+ this.lblObject = new System.Windows.Forms.Label();
+ this.cbxAreaObjectCode = new System.Windows.Forms.ComboBox();
+ this.lblY = new System.Windows.Forms.Label();
+ this.lblX = new System.Windows.Forms.Label();
+ this.nudY = new System.Windows.Forms.NumericUpDown();
+ this.nudX = new System.Windows.Forms.NumericUpDown();
+ this.gbxBinary = new System.Windows.Forms.GroupBox();
+ this.tbxManualInput = new System.Windows.Forms.TextBox();
+ this.chkUseManualInput = new System.Windows.Forms.CheckBox();
+ this.lblPage = new System.Windows.Forms.Label();
+ this.nudPage = new System.Windows.Forms.NumericUpDown();
+ this.groupBox1.SuspendLayout();
+ ((System.ComponentModel.ISupportInitialize)(this.nudLength)).BeginInit();
+ ((System.ComponentModel.ISupportInitialize)(this.nudY)).BeginInit();
+ ((System.ComponentModel.ISupportInitialize)(this.nudX)).BeginInit();
+ this.gbxBinary.SuspendLayout();
+ ((System.ComponentModel.ISupportInitialize)(this.nudPage)).BeginInit();
+ this.SuspendLayout();
+ //
+ // btnOK
+ //
+ this.btnOK.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right)));
+ this.btnOK.DialogResult = System.Windows.Forms.DialogResult.OK;
+ this.btnOK.Location = new System.Drawing.Point(253, 186);
+ this.btnOK.Name = "btnOK";
+ this.btnOK.Size = new System.Drawing.Size(75, 23);
+ this.btnOK.TabIndex = 0;
+ this.btnOK.Text = "&OK";
+ this.btnOK.UseVisualStyleBackColor = true;
+ //
+ // btnCancel
+ //
+ this.btnCancel.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right)));
+ this.btnCancel.DialogResult = System.Windows.Forms.DialogResult.Cancel;
+ this.btnCancel.Location = new System.Drawing.Point(334, 186);
+ this.btnCancel.Name = "btnCancel";
+ this.btnCancel.Size = new System.Drawing.Size(75, 23);
+ this.btnCancel.TabIndex = 1;
+ this.btnCancel.Text = "&Cancel";
+ this.btnCancel.UseVisualStyleBackColor = true;
+ //
+ // groupBox1
+ //
+ this.groupBox1.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)
+ | System.Windows.Forms.AnchorStyles.Right)));
+ this.groupBox1.Controls.Add(this.lblPage);
+ this.groupBox1.Controls.Add(this.nudPage);
+ this.groupBox1.Controls.Add(this.lblForegroundScenery);
+ this.groupBox1.Controls.Add(this.cbxBackgroundScenery);
+ this.groupBox1.Controls.Add(this.cbxForegroundScenery);
+ this.groupBox1.Controls.Add(this.lblBackgroundScenery);
+ this.groupBox1.Controls.Add(this.cbxTerrainMode);
+ this.groupBox1.Controls.Add(this.lblTerrainMode);
+ this.groupBox1.Controls.Add(this.nudLength);
+ this.groupBox1.Controls.Add(this.lblLength);
+ this.groupBox1.Controls.Add(this.lblObject);
+ this.groupBox1.Controls.Add(this.cbxAreaObjectCode);
+ this.groupBox1.Controls.Add(this.lblY);
+ this.groupBox1.Controls.Add(this.lblX);
+ this.groupBox1.Controls.Add(this.nudY);
+ this.groupBox1.Controls.Add(this.nudX);
+ this.groupBox1.Location = new System.Drawing.Point(12, 12);
+ this.groupBox1.Name = "groupBox1";
+ this.groupBox1.Size = new System.Drawing.Size(397, 148);
+ this.groupBox1.TabIndex = 2;
+ this.groupBox1.TabStop = false;
+ this.groupBox1.Text = "Object";
+ //
+ // lblForegroundScenery
+ //
+ this.lblForegroundScenery.AutoSize = true;
+ this.lblForegroundScenery.Location = new System.Drawing.Point(6, 124);
+ this.lblForegroundScenery.Name = "lblForegroundScenery";
+ this.lblForegroundScenery.Size = new System.Drawing.Size(61, 13);
+ this.lblForegroundScenery.TabIndex = 21;
+ this.lblForegroundScenery.Text = "Foreground";
+ //
+ // cbxBackgroundScenery
+ //
+ this.cbxBackgroundScenery.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)
+ | System.Windows.Forms.AnchorStyles.Right)));
+ this.cbxBackgroundScenery.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
+ this.cbxBackgroundScenery.FormattingEnabled = true;
+ this.cbxBackgroundScenery.Items.AddRange(new object[] {
+ "Nothing",
+ "Clouds",
+ "Mountain",
+ "Fence"});
+ this.cbxBackgroundScenery.Location = new System.Drawing.Point(82, 94);
+ this.cbxBackgroundScenery.Name = "cbxBackgroundScenery";
+ this.cbxBackgroundScenery.Size = new System.Drawing.Size(309, 21);
+ this.cbxBackgroundScenery.TabIndex = 20;
+ this.cbxBackgroundScenery.SelectedIndexChanged += new System.EventHandler(this.Item_ValueChanged);
+ //
+ // cbxForegroundScenery
+ //
+ this.cbxForegroundScenery.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)
+ | System.Windows.Forms.AnchorStyles.Right)));
+ this.cbxForegroundScenery.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
+ this.cbxForegroundScenery.FormattingEnabled = true;
+ this.cbxForegroundScenery.Items.AddRange(new object[] {
+ "None",
+ "Underwater",
+ "Castle Wall (Unused)",
+ "Over Water",
+ "Night (Unused)",
+ "Snow (Unused)",
+ "Night and Snow (Unused)",
+ "Castle (unused)"});
+ this.cbxForegroundScenery.Location = new System.Drawing.Point(82, 121);
+ this.cbxForegroundScenery.Name = "cbxForegroundScenery";
+ this.cbxForegroundScenery.Size = new System.Drawing.Size(309, 21);
+ this.cbxForegroundScenery.TabIndex = 19;
+ this.cbxForegroundScenery.SelectedIndexChanged += new System.EventHandler(this.Item_ValueChanged);
+ //
+ // lblBackgroundScenery
+ //
+ this.lblBackgroundScenery.AutoSize = true;
+ this.lblBackgroundScenery.Location = new System.Drawing.Point(6, 97);
+ this.lblBackgroundScenery.Name = "lblBackgroundScenery";
+ this.lblBackgroundScenery.Size = new System.Drawing.Size(46, 13);
+ this.lblBackgroundScenery.TabIndex = 18;
+ this.lblBackgroundScenery.Text = "Scenery";
+ //
+ // cbxTerrainMode
+ //
+ this.cbxTerrainMode.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)
+ | System.Windows.Forms.AnchorStyles.Right)));
+ this.cbxTerrainMode.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
+ this.cbxTerrainMode.FormattingEnabled = true;
+ this.cbxTerrainMode.Items.AddRange(new object[] {
+ "None",
+ "2-tile-high floor with no ceiling",
+ "2-tile-high floor with 1-tile-high ceiling",
+ "2-tile-high floor with 3-tile-high ceiling",
+ "2-tile-high floor with 4-tile-high ceiling",
+ "2-tile-high floor with 8-tile-high ceiling",
+ "5-tile-high floor with 1-tile-high ceiling",
+ "5-tile-high floor with 3-tile-high ceiling",
+ "5-tile-high floor with 4-tile-high ceiling",
+ "6-tile-high floor with 1-tile-high ceiling",
+ "No floor with 1-tile-high ceiling",
+ "6-tile-high floor with 4-tile-high ceiling",
+ "9-tile-high floor with 1-tile-high ceiling",
+ "2-tile-high floor with 1-tile-high ceiling and 5 tiles in the middle",
+ "2-tile-high floor with 1-tile-high ceiling and 4 tiles in the middle",
+ "Floor tiles everywhere"});
+ this.cbxTerrainMode.Location = new System.Drawing.Point(82, 67);
+ this.cbxTerrainMode.Name = "cbxTerrainMode";
+ this.cbxTerrainMode.Size = new System.Drawing.Size(309, 21);
+ this.cbxTerrainMode.TabIndex = 17;
+ this.cbxTerrainMode.SelectedIndexChanged += new System.EventHandler(this.Item_ValueChanged);
+ //
+ // lblTerrainMode
+ //
+ this.lblTerrainMode.AutoSize = true;
+ this.lblTerrainMode.Location = new System.Drawing.Point(6, 70);
+ this.lblTerrainMode.Name = "lblTerrainMode";
+ this.lblTerrainMode.Size = new System.Drawing.Size(70, 13);
+ this.lblTerrainMode.TabIndex = 16;
+ this.lblTerrainMode.Text = "Terrain Mode";
+ //
+ // nudLength
+ //
+ this.nudLength.Location = new System.Drawing.Point(356, 14);
+ this.nudLength.Maximum = new decimal(new int[] {
+ 15,
+ 0,
+ 0,
+ 0});
+ this.nudLength.Minimum = new decimal(new int[] {
+ 1,
+ 0,
+ 0,
+ 0});
+ this.nudLength.Name = "nudLength";
+ this.nudLength.Size = new System.Drawing.Size(35, 20);
+ this.nudLength.TabIndex = 8;
+ this.nudLength.TextAlign = System.Windows.Forms.HorizontalAlignment.Center;
+ this.nudLength.Value = new decimal(new int[] {
+ 1,
+ 0,
+ 0,
+ 0});
+ this.nudLength.ValueChanged += new System.EventHandler(this.Item_ValueChanged);
+ //
+ // lblLength
+ //
+ this.lblLength.AutoSize = true;
+ this.lblLength.Location = new System.Drawing.Point(310, 16);
+ this.lblLength.Name = "lblLength";
+ this.lblLength.Size = new System.Drawing.Size(40, 13);
+ this.lblLength.TabIndex = 7;
+ this.lblLength.Text = "Length";
+ //
+ // lblObject
+ //
+ this.lblObject.AutoSize = true;
+ this.lblObject.Location = new System.Drawing.Point(6, 43);
+ this.lblObject.Name = "lblObject";
+ this.lblObject.Size = new System.Drawing.Size(38, 13);
+ this.lblObject.TabIndex = 6;
+ this.lblObject.Text = "Object";
+ //
+ // cbxAreaObjectCode
+ //
+ this.cbxAreaObjectCode.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)
+ | System.Windows.Forms.AnchorStyles.Right)));
+ this.cbxAreaObjectCode.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
+ this.cbxAreaObjectCode.FormattingEnabled = true;
+ this.cbxAreaObjectCode.Location = new System.Drawing.Point(50, 40);
+ this.cbxAreaObjectCode.Name = "cbxAreaObjectCode";
+ this.cbxAreaObjectCode.Size = new System.Drawing.Size(341, 21);
+ this.cbxAreaObjectCode.TabIndex = 5;
+ this.cbxAreaObjectCode.SelectedIndexChanged += new System.EventHandler(this.AreaObectCode_SelectedIndexChanged);
+ //
+ // lblY
+ //
+ this.lblY.AutoSize = true;
+ this.lblY.Location = new System.Drawing.Point(170, 16);
+ this.lblY.Name = "lblY";
+ this.lblY.Size = new System.Drawing.Size(34, 13);
+ this.lblY.TabIndex = 3;
+ this.lblY.Text = "Y pos";
+ //
+ // lblX
+ //
+ this.lblX.AutoSize = true;
+ this.lblX.Location = new System.Drawing.Point(6, 16);
+ this.lblX.Name = "lblX";
+ this.lblX.Size = new System.Drawing.Size(34, 13);
+ this.lblX.TabIndex = 2;
+ this.lblX.Text = "X pos";
+ //
+ // nudY
+ //
+ this.nudY.Location = new System.Drawing.Point(210, 14);
+ this.nudY.Maximum = new decimal(new int[] {
+ 11,
+ 0,
+ 0,
+ 0});
+ this.nudY.Name = "nudY";
+ this.nudY.Size = new System.Drawing.Size(35, 20);
+ this.nudY.TabIndex = 1;
+ this.nudY.TextAlign = System.Windows.Forms.HorizontalAlignment.Center;
+ this.nudY.ValueChanged += new System.EventHandler(this.Item_ValueChanged);
+ //
+ // nudX
+ //
+ this.nudX.Location = new System.Drawing.Point(50, 14);
+ this.nudX.Maximum = new decimal(new int[] {
+ 15,
+ 0,
+ 0,
+ 0});
+ this.nudX.Name = "nudX";
+ this.nudX.Size = new System.Drawing.Size(35, 20);
+ this.nudX.TabIndex = 0;
+ this.nudX.TextAlign = System.Windows.Forms.HorizontalAlignment.Center;
+ this.nudX.ValueChanged += new System.EventHandler(this.Item_ValueChanged);
+ //
+ // gbxBinary
+ //
+ this.gbxBinary.Controls.Add(this.tbxManualInput);
+ this.gbxBinary.Controls.Add(this.chkUseManualInput);
+ this.gbxBinary.Location = new System.Drawing.Point(12, 166);
+ this.gbxBinary.Name = "gbxBinary";
+ this.gbxBinary.Size = new System.Drawing.Size(188, 59);
+ this.gbxBinary.TabIndex = 3;
+ this.gbxBinary.TabStop = false;
+ //
+ // tbxManualInput
+ //
+ this.tbxManualInput.CharacterCasing = System.Windows.Forms.CharacterCasing.Upper;
+ this.tbxManualInput.Location = new System.Drawing.Point(9, 23);
+ this.tbxManualInput.MaxLength = 8;
+ this.tbxManualInput.Name = "tbxManualInput";
+ this.tbxManualInput.Size = new System.Drawing.Size(173, 20);
+ this.tbxManualInput.TabIndex = 1;
+ this.tbxManualInput.WordWrap = false;
+ this.tbxManualInput.TextChanged += new System.EventHandler(this.ManualInput_TextChanged);
+ //
+ // chkUseManualInput
+ //
+ this.chkUseManualInput.AutoSize = true;
+ this.chkUseManualInput.Location = new System.Drawing.Point(9, 0);
+ this.chkUseManualInput.Name = "chkUseManualInput";
+ this.chkUseManualInput.Size = new System.Drawing.Size(124, 17);
+ this.chkUseManualInput.TabIndex = 0;
+ this.chkUseManualInput.Text = "Enter value manually";
+ this.chkUseManualInput.UseVisualStyleBackColor = true;
+ this.chkUseManualInput.CheckedChanged += new System.EventHandler(this.UseManualInput_CheckedChanged);
+ //
+ // lblPage
+ //
+ this.lblPage.AutoSize = true;
+ this.lblPage.Location = new System.Drawing.Point(91, 16);
+ this.lblPage.Name = "lblPage";
+ this.lblPage.Size = new System.Drawing.Size(32, 13);
+ this.lblPage.TabIndex = 23;
+ this.lblPage.Text = "Page";
+ //
+ // nudPage
+ //
+ this.nudPage.Location = new System.Drawing.Point(129, 14);
+ this.nudPage.Maximum = new decimal(new int[] {
+ 31,
+ 0,
+ 0,
+ 0});
+ this.nudPage.Name = "nudPage";
+ this.nudPage.Size = new System.Drawing.Size(35, 20);
+ this.nudPage.TabIndex = 22;
+ this.nudPage.TextAlign = System.Windows.Forms.HorizontalAlignment.Center;
+ this.nudPage.ValueChanged += new System.EventHandler(this.Item_ValueChanged);
+ //
+ // ObjectEditorForm
+ //
+ this.AcceptButton = this.btnOK;
+ this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
+ this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
+ this.CancelButton = this.btnCancel;
+ this.ClientSize = new System.Drawing.Size(421, 237);
+ this.Controls.Add(this.gbxBinary);
+ this.Controls.Add(this.groupBox1);
+ this.Controls.Add(this.btnCancel);
+ this.Controls.Add(this.btnOK);
+ this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog;
+ this.MaximizeBox = false;
+ this.MinimizeBox = false;
+ this.MinimumSize = new System.Drawing.Size(390, 276);
+ this.Name = "ObjectEditorForm";
+ this.ShowIcon = false;
+ this.ShowInTaskbar = false;
+ this.Text = "Object Editor";
+ this.groupBox1.ResumeLayout(false);
+ this.groupBox1.PerformLayout();
+ ((System.ComponentModel.ISupportInitialize)(this.nudLength)).EndInit();
+ ((System.ComponentModel.ISupportInitialize)(this.nudY)).EndInit();
+ ((System.ComponentModel.ISupportInitialize)(this.nudX)).EndInit();
+ this.gbxBinary.ResumeLayout(false);
+ this.gbxBinary.PerformLayout();
+ ((System.ComponentModel.ISupportInitialize)(this.nudPage)).EndInit();
+ this.ResumeLayout(false);
+
+ }
+
+ #endregion
+
+ private System.Windows.Forms.Button btnOK;
+ private System.Windows.Forms.Button btnCancel;
+ private System.Windows.Forms.GroupBox groupBox1;
+ private System.Windows.Forms.NumericUpDown nudLength;
+ private System.Windows.Forms.Label lblLength;
+ private System.Windows.Forms.Label lblObject;
+ private System.Windows.Forms.ComboBox cbxAreaObjectCode;
+ private System.Windows.Forms.Label lblY;
+ private System.Windows.Forms.Label lblX;
+ private System.Windows.Forms.NumericUpDown nudY;
+ private System.Windows.Forms.NumericUpDown nudX;
+ private System.Windows.Forms.GroupBox gbxBinary;
+ private System.Windows.Forms.TextBox tbxManualInput;
+ private System.Windows.Forms.CheckBox chkUseManualInput;
+ private System.Windows.Forms.ComboBox cbxTerrainMode;
+ private System.Windows.Forms.Label lblTerrainMode;
+ private System.Windows.Forms.ComboBox cbxForegroundScenery;
+ private System.Windows.Forms.Label lblBackgroundScenery;
+ private System.Windows.Forms.Label lblForegroundScenery;
+ private System.Windows.Forms.ComboBox cbxBackgroundScenery;
+ private System.Windows.Forms.Label lblPage;
+ private System.Windows.Forms.NumericUpDown nudPage;
+ }
+}
diff --git a/src/Brutario.Win/Dialogs/BaseForms/ObjectEditorForm.cs b/src/Brutario.Win/Dialogs/BaseForms/ObjectEditorForm.cs
new file mode 100644
index 0000000..2c354f2
--- /dev/null
+++ b/src/Brutario.Win/Dialogs/BaseForms/ObjectEditorForm.cs
@@ -0,0 +1,590 @@
+//
+// Copyright (c) 2022 spel werdz rite. All rights reserved. Licensed under GNU Affero
+// General Public License. See LICENSE in project root for full license
+// information, or visit https://www.gnu.org/licenses/#AGPL
+//
+
+namespace Brutario.Win.Dialogs.BaseForms;
+
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Diagnostics;
+using System.Globalization;
+using System.Windows.Forms;
+
+using Core;
+
+using Maseya.Smas.Smb1;
+using Maseya.Smas.Smb1.AreaData.HeaderData;
+using Maseya.Smas.Smb1.AreaData.ObjectData;
+
+internal partial class ObjectEditorForm : Form
+{
+ private AreaPlatformType _areaPlatformType;
+
+ public ObjectEditorForm()
+ {
+ InitializeComponent();
+
+ for (var i = 0; i < Codes.Count; i++)
+ {
+ _ = cbxAreaObjectCode.Items.Add(Codes[i].BaseName());
+ }
+
+ SetCommandInternal(default);
+ }
+
+ public event EventHandler? AreaPlatformTypeChanged;
+
+ public event EventHandler? AreaObjectCommandChanged;
+
+ public AreaPlatformType AreaPlatformType
+ {
+ get
+ {
+ return _areaPlatformType;
+ }
+
+ set
+ {
+ if (AreaPlatformType == value)
+ {
+ return;
+ }
+
+ _areaPlatformType = value;
+
+ // Change the name of the area specific platform in the object
+ // combo box to match the new value.
+ var index = EnumIndexes[AreaObjectCode.AreaSpecificPlatform];
+ var code = value.ToObjectCode();
+ cbxAreaObjectCode.Items[index] = code.BaseName();
+
+ OnAreaPlatformTypeChanged(EventArgs.Empty);
+ }
+ }
+
+ public UIAreaObjectCommand AreaObjectCommand
+ {
+ get
+ {
+ return UICommand;
+ }
+
+ set
+ {
+ UICommand = value;
+ }
+ }
+
+ private bool IsValidInput
+ {
+ get
+ {
+ return btnOK.Enabled;
+ }
+
+ set
+ {
+ btnOK.Enabled = value;
+ }
+ }
+
+ private bool UseManualInput
+ {
+ get
+ {
+ return chkUseManualInput.Checked;
+ }
+
+ set
+ {
+ chkUseManualInput.Checked = value;
+ }
+ }
+
+ private bool UICommandIsUpdating
+ {
+ get;
+ set;
+ }
+
+ private int XPos
+ {
+ get
+ {
+ return (int)nudX.Value;
+ }
+
+ set
+ {
+ if (value == XPos)
+ {
+ return;
+ }
+
+ nudX.Value = value;
+ }
+ }
+
+ private int Page
+ {
+ get
+ {
+ return (int)nudPage.Value;
+ }
+
+ set
+ {
+ if (value == Page)
+ {
+ return;
+ }
+
+ nudPage.Value = value;
+ }
+ }
+
+ private bool YPosEnabled
+ {
+ get
+ {
+ return nudY.Enabled;
+ }
+
+ set
+ {
+ lblY.Enabled =
+ nudY.Enabled = value;
+ }
+ }
+
+ private int YPos
+ {
+ get
+ {
+ var y = (int)AreaObjectCode >> 8;
+ return (y is < 0x0C or 0x0F) ? (int)nudY.Value : y;
+ }
+
+ set
+ {
+ if (value == YPos)
+ {
+ return;
+ }
+
+ nudY.Value = value;
+ }
+ }
+
+ private int ObjectCodeIndex
+ {
+ get
+ {
+ return cbxAreaObjectCode.SelectedIndex;
+ }
+ }
+
+ private AreaObjectCode AreaObjectCode
+ {
+ get
+ {
+ return Codes[Math.Max(ObjectCodeIndex, 0)];
+ }
+
+ set
+ {
+ if (ObjectCodeIndex >= 0 && value == Codes[ObjectCodeIndex])
+ {
+ return;
+ }
+
+ cbxAreaObjectCode.SelectedIndex =
+ EnumIndexes.TryGetValue(value, out var index)
+ ? index
+ : -1;
+ }
+ }
+
+ private int Length
+ {
+ get
+ {
+ return nudLength.Enabled ? (int)nudLength.Value : 1;
+ }
+
+ set
+ {
+ if (value == Length || value > MaximumLength)
+ {
+ return;
+ }
+
+ nudLength.Value = value;
+ }
+ }
+
+ private bool LengthEnabled
+ {
+ get
+ {
+ return nudLength.Enabled;
+ }
+
+ set
+ {
+ lblLength.Enabled =
+ nudLength.Enabled = value;
+ }
+ }
+
+ private int MaximumLength
+ {
+ get
+ {
+ return (int)nudLength.Maximum;
+ }
+
+ set
+ {
+ nudLength.Maximum = value;
+ }
+ }
+
+ private TerrainMode TerrainMode
+ {
+ get
+ {
+ return (TerrainMode)cbxTerrainMode.SelectedIndex;
+ }
+
+ set
+ {
+ if (value == TerrainMode)
+ {
+ return;
+ }
+
+ cbxTerrainMode.SelectedIndex = (int)value;
+ }
+ }
+
+ private BackgroundScenery BackgroundScenery
+ {
+ get
+ {
+ return (BackgroundScenery)cbxBackgroundScenery.SelectedIndex;
+ }
+
+ set
+ {
+ if (value == BackgroundScenery)
+ {
+ return;
+ }
+
+ cbxBackgroundScenery.SelectedIndex = (int)value;
+ }
+ }
+
+ private bool TerrainAndBackgroundSceneryEnabled
+ {
+ get
+ {
+ return cbxTerrainMode.Enabled;
+ }
+
+ set
+ {
+ lblTerrainMode.Enabled =
+ cbxTerrainMode.Enabled =
+ lblBackgroundScenery.Enabled =
+ cbxBackgroundScenery.Enabled = value;
+ }
+ }
+
+ private ForegroundScenery ForegroundScenery
+ {
+ get
+ {
+ return (ForegroundScenery)cbxForegroundScenery.SelectedIndex;
+ }
+
+ set
+ {
+ if (value == ForegroundScenery)
+ {
+ return;
+ }
+
+ cbxForegroundScenery.SelectedIndex = (int)value;
+ }
+ }
+
+ private bool ForegroundSceneryEnabled
+ {
+ get
+ {
+ return cbxForegroundScenery.Enabled;
+ }
+
+ set
+ {
+ lblForegroundScenery.Enabled =
+ cbxForegroundScenery.Enabled = value;
+ }
+ }
+
+ private UIAreaObjectCommand UICommand
+ {
+ get
+ {
+ var result = default(AreaObjectCommand);
+ result.Value1 |= (byte)(XPos << 4);
+ switch ((int)AreaObjectCode & 0xF00)
+ {
+ case 0xE00:
+ result.Value1 |= 0x0E;
+ result.Value2 |= (byte)(((int)AreaObjectCode) & 0x40);
+ if (ForegroundSceneryEnabled)
+ {
+ result.Value2 |= (byte)ForegroundScenery;
+ }
+ else if (TerrainAndBackgroundSceneryEnabled)
+ {
+ result.Value2 |= (byte)TerrainMode;
+ result.Value2 |= (byte)((int)BackgroundScenery << 4);
+ }
+ else
+ {
+ Debug.Assert(
+ false,
+ "Scenery command but no scenery objects enabled.");
+ }
+
+ break;
+
+ case 0xF00:
+ result.Value1 |= 0x0F;
+ if (YPosEnabled)
+ {
+ result.Value2 |= (byte)(YPos << 4);
+ }
+
+ result.Value3 |= (byte)((int)AreaObjectCode & 0x7F);
+ break;
+
+ default:
+ if (YPosEnabled)
+ {
+ result.Value1 |= (byte)YPos;
+ }
+
+ result.Value1 |= (byte)((int)AreaObjectCode >> 8);
+ result.Value2 |= (byte)((int)AreaObjectCode & 0x7F);
+ break;
+ }
+
+ if (LengthEnabled)
+ {
+ result.Value2 |= (byte)(Length - 1);
+ }
+
+ return new UIAreaObjectCommand(result, Page);
+ }
+
+ set
+ {
+ if (value == UICommand)
+ {
+ return;
+ }
+
+ SetCommandInternal(value);
+ OnAreaObjectCommandChanged(EventArgs.Empty);
+ }
+ }
+
+ private UIAreaObjectCommand BinaryCommand
+ {
+ get
+ {
+ _ = TryGetBinaryCommand(tbxManualInput.Text, out var result);
+ return result;
+ }
+
+ set
+ {
+ if (TryGetBinaryCommand(tbxManualInput.Text, out var result)
+ && value == result)
+ {
+ return;
+ }
+
+ tbxManualInput.Text = value.HexString;
+ }
+ }
+
+ private static IReadOnlyList Codes
+ {
+ get
+ {
+ return Maseya.Smas.Smb1.AreaData.ObjectData.AreaObjectCommand.ValidCodes;
+ }
+ }
+
+ private static ReadOnlyDictionary EnumIndexes { get; } = new(
+ new Dictionary(
+ Enumerable.Range(0, Codes.Count)
+ .Select(i => new KeyValuePair(Codes[i], i))));
+
+ private static bool TryGetBinaryCommand(string text, out UIAreaObjectCommand command)
+ {
+ var tokens = text.Split(' ', StringSplitOptions.RemoveEmptyEntries);
+ if (tokens.Length is not 4 and not 3)
+ {
+ command = default;
+ return false;
+ }
+
+ var bytes = new byte[4];
+ for (var i = 0; i < tokens.Length; i++)
+ {
+ if (tokens[i].Length != 2)
+ {
+ command = default;
+ return false;
+ }
+
+ if (!Byte.TryParse(
+ tokens[i],
+ NumberStyles.HexNumber,
+ CultureInfo.CurrentUICulture,
+ out bytes[i]))
+ {
+ command = default;
+ return false;
+ }
+ }
+
+ var result = new AreaObjectCommand(bytes[1], bytes[2], bytes[3]);
+ if (!result.IsValid || bytes[0] >= 0x20
+ || result.Code == AreaObjectCode.ScreenJump)
+ {
+ command = default;
+ return false;
+ }
+
+ command = new UIAreaObjectCommand(result, bytes[0]);
+ return true;
+ }
+
+ private void UpdateEnabledControls(AreaObjectCommand value)
+ {
+ YPosEnabled = value.HasYCoord;
+ LengthEnabled = value.IsExtendableObject;
+ MaximumLength = 1 + value.Code.GetMaxLength();
+ TerrainAndBackgroundSceneryEnabled = value.IsTerrainAndBackgroundChange;
+ ForegroundSceneryEnabled = value.IsForegroundChange;
+ }
+
+ private void SetCommandInternal(UIAreaObjectCommand value)
+ {
+ Debug.Assert(!UICommandIsUpdating, "Object command is being set recursively");
+
+ UICommandIsUpdating = true;
+ var command = value.Command;
+ UpdateEnabledControls(command);
+
+ XPos = command.X;
+ Page = value.Page;
+ if (YPosEnabled)
+ {
+ YPos = command.Y;
+ }
+
+ AreaObjectCode = command.Code;
+ if (TerrainAndBackgroundSceneryEnabled)
+ {
+ TerrainMode = command.TerrainMode;
+ BackgroundScenery = command.BackgroundScenery;
+ }
+ else
+ {
+ TerrainMode = default;
+ BackgroundScenery = default;
+ }
+
+ ForegroundScenery = ForegroundSceneryEnabled
+ ? command.ForegroundScenery
+ : default;
+
+ Length = LengthEnabled ? 1 + command.Length : 1;
+
+ if (!UseManualInput)
+ {
+ BinaryCommand = UICommand;
+ }
+
+ UICommandIsUpdating = false;
+ }
+
+ private void UpdateValidInputFlag()
+ {
+ // If we're using the list and check boxes, then the input is always valid by
+ // their restraints. Otherwise, if we're entering the value manually, then we
+ // must check that text is valid.
+ IsValidInput =
+ !UseManualInput || TryGetBinaryCommand(tbxManualInput.Text, out var _);
+ }
+
+ private void ManualInput_TextChanged(object? sender, EventArgs e)
+ {
+ UpdateValidInputFlag();
+ if (!UICommandIsUpdating && IsValidInput)
+ {
+ UICommand = BinaryCommand;
+ }
+ }
+
+ private void AreaObectCode_SelectedIndexChanged(object? sender, EventArgs e)
+ {
+ if (UICommandIsUpdating)
+ {
+ return;
+ }
+
+ UpdateEnabledControls(UICommand.Command);
+ BinaryCommand = UICommand;
+ OnAreaObjectCommandChanged(EventArgs.Empty);
+ }
+
+ private void Item_ValueChanged(object sender, EventArgs e)
+ {
+ var control = (Control)sender;
+ if (!control.Enabled || UICommandIsUpdating)
+ {
+ return;
+ }
+
+ BinaryCommand = UICommand;
+ OnAreaObjectCommandChanged(EventArgs.Empty);
+ }
+
+ private void UseManualInput_CheckedChanged(object? sender, EventArgs e)
+ {
+ UpdateValidInputFlag();
+ }
+
+ private void OnAreaPlatformTypeChanged(EventArgs e)
+ {
+ AreaPlatformTypeChanged?.Invoke(this, e);
+ }
+
+ private void OnAreaObjectCommandChanged(EventArgs e)
+ {
+ AreaObjectCommandChanged?.Invoke(this, e);
+ }
+}
diff --git a/src/Brutario.Win/Dialogs/BaseForms/ObjectEditorForm.resx b/src/Brutario.Win/Dialogs/BaseForms/ObjectEditorForm.resx
new file mode 100644
index 0000000..1af7de1
--- /dev/null
+++ b/src/Brutario.Win/Dialogs/BaseForms/ObjectEditorForm.resx
@@ -0,0 +1,120 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
\ No newline at end of file
diff --git a/src/Brutario.Win/Dialogs/BaseForms/ObjectListForm.Designer.cs b/src/Brutario.Win/Dialogs/BaseForms/ObjectListForm.Designer.cs
new file mode 100644
index 0000000..aa050c0
--- /dev/null
+++ b/src/Brutario.Win/Dialogs/BaseForms/ObjectListForm.Designer.cs
@@ -0,0 +1,203 @@
+//
+// Copyright (c) 2022 spel werdz rite. All rights reserved. Licensed
+// under GNU Affero General Public License. See LICENSE in project
+// root for full license information, or visit
+// https://www.gnu.org/licenses/#AGPL
+//
+
+namespace Brutario.Win.Dialogs.BaseForms
+{
+ partial class ObjectListForm
+ {
+ ///
+ /// Required designer variable.
+ ///
+ private System.ComponentModel.IContainer components = null;
+
+ ///
+ /// Clean up any resources being used.
+ ///
+ /// true if managed resources should be disposed; otherwise, false.
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing && (components != null))
+ {
+ components.Dispose();
+ }
+ base.Dispose(disposing);
+ }
+
+ #region Windows Form Designer generated code
+
+ ///
+ /// Required method for Designer support - do not modify
+ /// the contents of this method with the code editor.
+ ///
+ private void InitializeComponent()
+ {
+ tlsMenu = new ToolStrip();
+ tsbAdd = new ToolStripButton();
+ tsbDelete = new ToolStripButton();
+ tsbClear = new ToolStripButton();
+ toolStripButton1 = new ToolStripButton();
+ toolStripButton2 = new ToolStripButton();
+ toolStripButton3 = new ToolStripButton();
+ lvwObjects = new Controls.ListViewNF();
+ chdHex = new ColumnHeader();
+ chdPage = new ColumnHeader();
+ chdPosition = new ColumnHeader();
+ chdType = new ColumnHeader();
+ tlsMenu.SuspendLayout();
+ SuspendLayout();
+ //
+ // tlsMenu
+ //
+ tlsMenu.GripStyle = ToolStripGripStyle.Hidden;
+ tlsMenu.ImageScalingSize = new Size(20, 20);
+ tlsMenu.Items.AddRange(new ToolStripItem[] { tsbAdd, tsbDelete, tsbClear, toolStripButton1, toolStripButton2, toolStripButton3 });
+ tlsMenu.Location = new Point(0, 0);
+ tlsMenu.Name = "tlsMenu";
+ tlsMenu.Size = new Size(661, 27);
+ tlsMenu.TabIndex = 1;
+ tlsMenu.Text = "...";
+ //
+ // tsbAdd
+ //
+ tsbAdd.DisplayStyle = ToolStripItemDisplayStyle.Image;
+ tsbAdd.Image = Properties.Resources.plus_solid;
+ tsbAdd.ImageTransparentColor = Color.Magenta;
+ tsbAdd.Name = "tsbAdd";
+ tsbAdd.Size = new Size(29, 24);
+ tsbAdd.Text = "Add";
+ tsbAdd.Click += Add_Click;
+ //
+ // tsbDelete
+ //
+ tsbDelete.DisplayStyle = ToolStripItemDisplayStyle.Image;
+ tsbDelete.Image = Properties.Resources.minus_solid;
+ tsbDelete.ImageTransparentColor = Color.Magenta;
+ tsbDelete.Name = "tsbDelete";
+ tsbDelete.Size = new Size(29, 24);
+ tsbDelete.Text = "Delete";
+ tsbDelete.Click += Delete_Click;
+ //
+ // tsbClear
+ //
+ tsbClear.DisplayStyle = ToolStripItemDisplayStyle.Image;
+ tsbClear.Image = Properties.Resources.trash_solid;
+ tsbClear.ImageTransparentColor = Color.Magenta;
+ tsbClear.Name = "tsbClear";
+ tsbClear.Size = new Size(29, 24);
+ tsbClear.Text = "Clear";
+ tsbClear.Click += Clear_Click;
+ //
+ // toolStripButton1
+ //
+ toolStripButton1.DisplayStyle = ToolStripItemDisplayStyle.Image;
+ toolStripButton1.Image = Properties.Resources.Picture1;
+ toolStripButton1.ImageTransparentColor = Color.Magenta;
+ toolStripButton1.Name = "toolStripButton1";
+ toolStripButton1.Size = new Size(29, 24);
+ toolStripButton1.Text = "toolStripButton1";
+ toolStripButton1.Click += MoveDown_Click;
+ //
+ // toolStripButton2
+ //
+ toolStripButton2.DisplayStyle = ToolStripItemDisplayStyle.Image;
+ toolStripButton2.Image = Properties.Resources.Picture2;
+ toolStripButton2.ImageTransparentColor = Color.Magenta;
+ toolStripButton2.Name = "toolStripButton2";
+ toolStripButton2.Size = new Size(29, 24);
+ toolStripButton2.Text = "toolStripButton2";
+ toolStripButton2.Click += MoveUp_Click;
+ //
+ // toolStripButton3
+ //
+ toolStripButton3.DisplayStyle = ToolStripItemDisplayStyle.Image;
+ toolStripButton3.Enabled = false;
+ toolStripButton3.Image = Properties.Resources.circle_question_regular;
+ toolStripButton3.ImageTransparentColor = Color.Magenta;
+ toolStripButton3.Name = "toolStripButton3";
+ toolStripButton3.Size = new Size(29, 24);
+ toolStripButton3.Text = "toolStripButton3";
+ //
+ // lvwObjects
+ //
+ lvwObjects.Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right;
+ lvwObjects.AutoArrange = false;
+ lvwObjects.Columns.AddRange(new ColumnHeader[] { chdHex, chdPage, chdPosition, chdType });
+ lvwObjects.FullRowSelect = true;
+ lvwObjects.GridLines = true;
+ lvwObjects.HeaderStyle = ColumnHeaderStyle.Nonclickable;
+ lvwObjects.LabelWrap = false;
+ lvwObjects.Location = new Point(0, 43);
+ lvwObjects.Margin = new Padding(4, 5, 4, 5);
+ lvwObjects.MultiSelect = false;
+ lvwObjects.Name = "lvwObjects";
+ lvwObjects.ShowGroups = false;
+ lvwObjects.Size = new Size(660, 469);
+ lvwObjects.TabIndex = 0;
+ lvwObjects.UseCompatibleStateImageBehavior = false;
+ lvwObjects.View = View.Details;
+ lvwObjects.ItemSelectionChanged += Objects_ItemSelectionChanged;
+ lvwObjects.SelectedIndexChanged += Objects_SelectedIndexChanged;
+ lvwObjects.MouseDoubleClick += Objects_MouseDoubleClick;
+ //
+ // chdHex
+ //
+ chdHex.Text = "Hex";
+ chdHex.Width = 57;
+ //
+ // chdPage
+ //
+ chdPage.Text = "Page";
+ chdPage.Width = 63;
+ //
+ // chdPosition
+ //
+ chdPosition.Text = "Position";
+ chdPosition.Width = 70;
+ //
+ // chdType
+ //
+ chdType.Text = "Type";
+ chdType.Width = 600;
+ //
+ // ObjectListForm
+ //
+ AutoScaleDimensions = new SizeF(8F, 20F);
+ AutoScaleMode = AutoScaleMode.Font;
+ ClientSize = new Size(661, 514);
+ Controls.Add(tlsMenu);
+ Controls.Add(lvwObjects);
+ Margin = new Padding(4, 5, 4, 5);
+ MaximizeBox = false;
+ MinimizeBox = false;
+ MinimumSize = new Size(677, 549);
+ Name = "ObjectListForm";
+ ShowIcon = false;
+ ShowInTaskbar = false;
+ StartPosition = FormStartPosition.CenterScreen;
+ Text = "Object List";
+ tlsMenu.ResumeLayout(false);
+ tlsMenu.PerformLayout();
+ ResumeLayout(false);
+ PerformLayout();
+ }
+
+ #endregion
+
+ private Controls.ListViewNF lvwObjects;
+ private System.Windows.Forms.ColumnHeader chdHex;
+ private System.Windows.Forms.ColumnHeader chdPage;
+ private System.Windows.Forms.ColumnHeader chdPosition;
+ private System.Windows.Forms.ColumnHeader chdType;
+ private System.Windows.Forms.ToolStrip tlsMenu;
+ private System.Windows.Forms.ToolStripButton tsbAdd;
+ private System.Windows.Forms.ToolStripButton tsbDelete;
+ private System.Windows.Forms.ToolStripButton tsbClear;
+ private ToolStripButton toolStripButton1;
+ private ToolStripButton toolStripButton2;
+ private ToolStripButton toolStripButton3;
+ }
+}
diff --git a/src/Brutario.Win/Dialogs/BaseForms/ObjectListForm.cs b/src/Brutario.Win/Dialogs/BaseForms/ObjectListForm.cs
new file mode 100644
index 0000000..c8f39ae
--- /dev/null
+++ b/src/Brutario.Win/Dialogs/BaseForms/ObjectListForm.cs
@@ -0,0 +1,325 @@
+//
+// Copyright (c) 2022 spel werdz rite. All rights reserved. Licensed under GNU Affero
+// General Public License. See LICENSE in project root for full license
+// information, or visit https://www.gnu.org/licenses/#AGPL
+//
+
+namespace Brutario.Win.Dialogs.BaseForms;
+
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Windows.Forms;
+
+using Brutario.Core;
+
+using Maseya.Smas.Smb1;
+using Maseya.Smas.Smb1.AreaData.ObjectData;
+
+public sealed partial class ObjectListForm : Form
+{
+ private AreaPlatformType _areaPlatformType;
+
+ public ObjectListForm()
+ {
+ InitializeComponent();
+
+ Items = new ItemCollection(this);
+ }
+
+ public event EventHandler? AreaPlatformTypeChanged;
+
+ public event EventHandler? SelectedIndexChanged;
+
+ public event EventHandler? EditItem;
+
+ public event EventHandler? AddItem_Click;
+
+ public event EventHandler? DeleteItem_Click;
+
+ public event EventHandler? ClearItems_Click;
+
+ public event EventHandler? MoveItemDown_Click;
+
+ public event EventHandler? MoveItemUp_Click;
+
+ public AreaPlatformType AreaPlatformType
+ {
+ get
+ {
+ return _areaPlatformType;
+ }
+
+ set
+ {
+ if (_areaPlatformType == value)
+ {
+ return;
+ }
+
+ _areaPlatformType = value;
+ OnAreaPlatformTypeChanged(EventArgs.Empty);
+ }
+ }
+
+ public int SelectedIndex
+ {
+ get
+ {
+ return lvwObjects.SelectedIndices.Count > 0
+ ? lvwObjects.SelectedIndices[0]
+ : -1;
+ }
+
+ set
+ {
+ if ((uint)value < (uint)lvwObjects.Items.Count)
+ {
+ lvwObjects.Items[value].Selected = true;
+ }
+ else
+ {
+ lvwObjects.SelectedIndices.Clear();
+ }
+ }
+ }
+
+ public ItemCollection Items
+ {
+ get;
+ }
+
+ private void UpdateAreaPlatformDescriptions()
+ {
+ for (var i = 0; i < Items.Count; i++)
+ {
+ if (Items[i].Command.Code != AreaObjectCode.AreaSpecificPlatform)
+ {
+ continue;
+ }
+
+ // TODO(swr): Rewrites the description. It's totally hacky
+ Items[i] = Items[i];
+ }
+ }
+
+ private void OnAreaPlatformTypeChanged(EventArgs e)
+ {
+ UpdateAreaPlatformDescriptions();
+ AreaPlatformTypeChanged?.Invoke(this, e);
+ }
+
+ private void OnSelectedIndexChanged(EventArgs e)
+ {
+ SelectedIndexChanged?.Invoke(this, e);
+ }
+
+ private void OnEditItem(EventArgs e)
+ {
+ EditItem?.Invoke(this, e);
+ }
+
+ private void Add_Click(object? sender, EventArgs e)
+ {
+ AddItem_Click?.Invoke(this, EventArgs.Empty);
+ }
+
+ private void Delete_Click(object? sender, EventArgs e)
+ {
+ DeleteItem_Click?.Invoke(this, EventArgs.Empty);
+ }
+
+ private void Clear_Click(object? sender, EventArgs e)
+ {
+ ClearItems_Click?.Invoke(this, EventArgs.Empty);
+ }
+
+ private void MoveDown_Click(object? sender, EventArgs e)
+ {
+ MoveItemDown_Click?.Invoke(this, EventArgs.Empty);
+ }
+
+ private void MoveUp_Click(object? sender, EventArgs e)
+ {
+ MoveItemUp_Click?.Invoke(this, EventArgs.Empty);
+ }
+
+ private void Objects_MouseDoubleClick(object? sender, MouseEventArgs e)
+ {
+ if (lvwObjects.SelectedItems.Count != 1)
+ {
+ return;
+ }
+
+ OnEditItem(EventArgs.Empty);
+ }
+
+ private void Objects_SelectedIndexChanged(object? sender, EventArgs e)
+ {
+ OnSelectedIndexChanged(EventArgs.Empty);
+ }
+
+ private void Objects_ItemSelectionChanged(
+ object? sender,
+ ListViewItemSelectionChangedEventArgs e)
+ {
+ OnSelectedIndexChanged(EventArgs.Empty);
+ }
+
+ public class ItemCollection : IList
+ {
+ public ItemCollection(ObjectListForm owner)
+ {
+ Owner = owner;
+ }
+
+ public int Count
+ {
+ get
+ {
+ return BaseItems.Count;
+ }
+ }
+
+ bool ICollection.IsReadOnly
+ {
+ get
+ {
+ return false;
+ }
+ }
+
+ private ObjectListForm Owner
+ {
+ get;
+ }
+
+ private ListView.ListViewItemCollection BaseItems
+ {
+ get
+ {
+ return Owner.lvwObjects.Items;
+ }
+ }
+
+ public UIAreaObjectCommand this[int index]
+ {
+ get
+ {
+ return (UIAreaObjectCommand)BaseItems[index].Tag;
+ }
+
+ set
+ {
+ BaseItems[index].Tag = value;
+ BaseItems[index].SubItems[0].Text = value.Command.HexString;
+ BaseItems[index].SubItems[1].Text = $"{value.Page}";
+ BaseItems[index].SubItems[2].Text = $"{value.X:X1},{value.Y:X1}";
+ BaseItems[index].SubItems[3].Text = value.Command.GetDescription(
+ Owner.AreaPlatformType);
+ }
+ }
+
+ public void Add(UIAreaObjectCommand item)
+ {
+ Insert(Count, item);
+ }
+
+ public void MoveItem(int oldIndex, int newIndex)
+ {
+ var change = Math.Sign(newIndex - oldIndex);
+ var temp = this[oldIndex];
+ for (var i = oldIndex; i != newIndex; i += change)
+ {
+ this[i] = this[i + change];
+ }
+
+ this[newIndex] = temp;
+ }
+
+ public void Insert(int index, UIAreaObjectCommand item)
+ {
+ _ = BaseItems.Insert(index, Create(item));
+ }
+
+ public void Clear()
+ {
+ BaseItems.Clear();
+ }
+
+ public bool Contains(UIAreaObjectCommand item)
+ {
+ return IndexOf(item) != -1;
+ }
+
+ public int IndexOf(UIAreaObjectCommand item)
+ {
+ for (var i = 0; i < Count; i++)
+ {
+ if (item.Equals(this[i]))
+ {
+ return i;
+ }
+ }
+
+ return -1;
+ }
+
+ public bool Remove(UIAreaObjectCommand item)
+ {
+ var index = IndexOf(item);
+ if (index == -1)
+ {
+ return false;
+ }
+
+ RemoveAt(index);
+ return true;
+ }
+
+ public void RemoveAt(int index)
+ {
+ BaseItems.RemoveAt(index);
+ }
+
+ public IEnumerator GetEnumerator()
+ {
+ foreach (var item in BaseItems)
+ {
+ yield return (UIAreaObjectCommand)(item as ListViewItem)!.Tag;
+ }
+ }
+
+ void ICollection.CopyTo(
+ UIAreaObjectCommand[] dest,
+ int index)
+ {
+ if ((uint)(index + Count) < (uint)dest.Length || index < 0)
+ {
+ throw new ArgumentOutOfRangeException(nameof(index));
+ }
+
+ for (var i = 0; i < Count; i++)
+ {
+ dest[index + i] = this[i];
+ }
+ }
+
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return GetEnumerator();
+ }
+
+ private ListViewItem Create(UIAreaObjectCommand item)
+ {
+ return new ListViewItem(new string[] {
+ item.Command.HexString,
+ $"{item.Page}",
+ $"{item.X:X1},{item.Y:X1}",
+ item.Command.GetDescription(Owner.AreaPlatformType)})
+ {
+ Tag = item
+ };
+ }
+ }
+}
diff --git a/src/Brutario.Win/Dialogs/BaseForms/ObjectListForm.resx b/src/Brutario.Win/Dialogs/BaseForms/ObjectListForm.resx
new file mode 100644
index 0000000..f6e65b7
--- /dev/null
+++ b/src/Brutario.Win/Dialogs/BaseForms/ObjectListForm.resx
@@ -0,0 +1,123 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ 17, 17
+
+
\ No newline at end of file
diff --git a/src/Brutario.Win/Dialogs/BaseForms/SpriteEditorForm.Designer.cs b/src/Brutario.Win/Dialogs/BaseForms/SpriteEditorForm.Designer.cs
new file mode 100644
index 0000000..3dd729d
--- /dev/null
+++ b/src/Brutario.Win/Dialogs/BaseForms/SpriteEditorForm.Designer.cs
@@ -0,0 +1,372 @@
+//
+// Copyright (c) 2022 spel werdz rite. All rights reserved. Licensed
+// under GNU Affero General Public License. See LICENSE in project
+// root for full license information, or visit
+// https://www.gnu.org/licenses/#AGPL
+//
+
+namespace Brutario.Win.Dialogs.BaseForms
+{
+ partial class SpriteEditorForm
+ {
+ ///
+ /// Required designer variable.
+ ///
+ private System.ComponentModel.IContainer components = null;
+
+ ///
+ /// Clean up any resources being used.
+ ///
+ /// true if managed resources should be disposed; otherwise, false.
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing && (components != null))
+ {
+ components.Dispose();
+ }
+ base.Dispose(disposing);
+ }
+
+ #region Windows Form Designer generated code
+
+ ///
+ /// Required method for Designer support - do not modify
+ /// the contents of this method with the code editor.
+ ///
+ private void InitializeComponent()
+ {
+ gbxBinary = new GroupBox();
+ tbxManualInput = new TextBox();
+ chkUseManualInput = new CheckBox();
+ btnCancel = new Button();
+ btnOK = new Button();
+ groupBox1 = new GroupBox();
+ lblPage = new Label();
+ nudPage = new NumericUpDown();
+ tbxAreaNumber = new TextBox();
+ lblAreaNumber = new Label();
+ nudWorld = new NumericUpDown();
+ lblWorld = new Label();
+ lblDestPage = new Label();
+ nudDestPage = new NumericUpDown();
+ chkHardFlag = new CheckBox();
+ lblObject = new Label();
+ cbxAreaSpriteCode = new ComboBox();
+ lblY = new Label();
+ lblX = new Label();
+ nudY = new NumericUpDown();
+ nudX = new NumericUpDown();
+ gbxBinary.SuspendLayout();
+ groupBox1.SuspendLayout();
+ ((System.ComponentModel.ISupportInitialize)nudPage).BeginInit();
+ ((System.ComponentModel.ISupportInitialize)nudWorld).BeginInit();
+ ((System.ComponentModel.ISupportInitialize)nudDestPage).BeginInit();
+ ((System.ComponentModel.ISupportInitialize)nudY).BeginInit();
+ ((System.ComponentModel.ISupportInitialize)nudX).BeginInit();
+ SuspendLayout();
+ //
+ // gbxBinary
+ //
+ gbxBinary.Controls.Add(tbxManualInput);
+ gbxBinary.Controls.Add(chkUseManualInput);
+ gbxBinary.Location = new Point(16, 171);
+ gbxBinary.Margin = new Padding(4, 5, 4, 5);
+ gbxBinary.Name = "gbxBinary";
+ gbxBinary.Padding = new Padding(4, 5, 4, 5);
+ gbxBinary.Size = new Size(251, 91);
+ gbxBinary.TabIndex = 6;
+ gbxBinary.TabStop = false;
+ //
+ // tbxManualInput
+ //
+ tbxManualInput.CharacterCasing = CharacterCasing.Upper;
+ tbxManualInput.Location = new Point(12, 35);
+ tbxManualInput.Margin = new Padding(4, 5, 4, 5);
+ tbxManualInput.MaxLength = 8;
+ tbxManualInput.Name = "tbxManualInput";
+ tbxManualInput.Size = new Size(229, 27);
+ tbxManualInput.TabIndex = 1;
+ tbxManualInput.WordWrap = false;
+ tbxManualInput.TextChanged += ManualInput_TextChanged;
+ //
+ // chkUseManualInput
+ //
+ chkUseManualInput.AutoSize = true;
+ chkUseManualInput.Location = new Point(12, 0);
+ chkUseManualInput.Margin = new Padding(4, 5, 4, 5);
+ chkUseManualInput.Name = "chkUseManualInput";
+ chkUseManualInput.Size = new Size(168, 24);
+ chkUseManualInput.TabIndex = 0;
+ chkUseManualInput.Text = "Enter value manually";
+ chkUseManualInput.UseVisualStyleBackColor = true;
+ chkUseManualInput.CheckedChanged += UseManualInput_CheckedChanged;
+ //
+ // btnCancel
+ //
+ btnCancel.DialogResult = DialogResult.Cancel;
+ btnCancel.Location = new Point(383, 202);
+ btnCancel.Margin = new Padding(4, 5, 4, 5);
+ btnCancel.Name = "btnCancel";
+ btnCancel.Size = new Size(100, 35);
+ btnCancel.TabIndex = 5;
+ btnCancel.Text = "&Cancel";
+ btnCancel.UseVisualStyleBackColor = true;
+ //
+ // btnOK
+ //
+ btnOK.DialogResult = DialogResult.OK;
+ btnOK.Location = new Point(275, 202);
+ btnOK.Margin = new Padding(4, 5, 4, 5);
+ btnOK.Name = "btnOK";
+ btnOK.Size = new Size(100, 35);
+ btnOK.TabIndex = 4;
+ btnOK.Text = "&OK";
+ btnOK.UseVisualStyleBackColor = true;
+ //
+ // groupBox1
+ //
+ groupBox1.Controls.Add(lblPage);
+ groupBox1.Controls.Add(nudPage);
+ groupBox1.Controls.Add(tbxAreaNumber);
+ groupBox1.Controls.Add(lblAreaNumber);
+ groupBox1.Controls.Add(nudWorld);
+ groupBox1.Controls.Add(lblWorld);
+ groupBox1.Controls.Add(lblDestPage);
+ groupBox1.Controls.Add(nudDestPage);
+ groupBox1.Controls.Add(chkHardFlag);
+ groupBox1.Controls.Add(lblObject);
+ groupBox1.Controls.Add(cbxAreaSpriteCode);
+ groupBox1.Controls.Add(lblY);
+ groupBox1.Controls.Add(lblX);
+ groupBox1.Controls.Add(nudY);
+ groupBox1.Controls.Add(nudX);
+ groupBox1.Location = new Point(16, 18);
+ groupBox1.Margin = new Padding(4, 5, 4, 5);
+ groupBox1.Name = "groupBox1";
+ groupBox1.Padding = new Padding(4, 5, 4, 5);
+ groupBox1.Size = new Size(467, 143);
+ groupBox1.TabIndex = 7;
+ groupBox1.TabStop = false;
+ groupBox1.Text = "Object";
+ //
+ // lblPage
+ //
+ lblPage.AutoSize = true;
+ lblPage.Location = new Point(121, 25);
+ lblPage.Margin = new Padding(4, 0, 4, 0);
+ lblPage.Name = "lblPage";
+ lblPage.Size = new Size(41, 20);
+ lblPage.TabIndex = 29;
+ lblPage.Text = "Page";
+ //
+ // nudPage
+ //
+ nudPage.Location = new Point(172, 22);
+ nudPage.Margin = new Padding(4, 5, 4, 5);
+ nudPage.Maximum = new decimal(new int[] { 31, 0, 0, 0 });
+ nudPage.Name = "nudPage";
+ nudPage.Size = new Size(47, 27);
+ nudPage.TabIndex = 28;
+ nudPage.TextAlign = HorizontalAlignment.Center;
+ nudPage.Value = new decimal(new int[] { 1, 0, 0, 0 });
+ nudPage.ValueChanged += Item_ValueChanged;
+ //
+ // tbxAreaNumber
+ //
+ tbxAreaNumber.CharacterCasing = CharacterCasing.Upper;
+ tbxAreaNumber.Location = new Point(420, 102);
+ tbxAreaNumber.Margin = new Padding(4, 5, 4, 5);
+ tbxAreaNumber.MaxLength = 2;
+ tbxAreaNumber.Name = "tbxAreaNumber";
+ tbxAreaNumber.Size = new Size(37, 27);
+ tbxAreaNumber.TabIndex = 2;
+ tbxAreaNumber.Text = "25";
+ tbxAreaNumber.TextAlign = HorizontalAlignment.Right;
+ tbxAreaNumber.WordWrap = false;
+ tbxAreaNumber.TextChanged += AreaNumber_TextChanged;
+ //
+ // lblAreaNumber
+ //
+ lblAreaNumber.AutoSize = true;
+ lblAreaNumber.Location = new Point(320, 106);
+ lblAreaNumber.Margin = new Padding(4, 0, 4, 0);
+ lblAreaNumber.Name = "lblAreaNumber";
+ lblAreaNumber.Size = new Size(98, 20);
+ lblAreaNumber.TabIndex = 27;
+ lblAreaNumber.Text = "Area Number";
+ //
+ // nudWorld
+ //
+ nudWorld.Location = new Point(201, 103);
+ nudWorld.Margin = new Padding(4, 5, 4, 5);
+ nudWorld.Maximum = new decimal(new int[] { 8, 0, 0, 0 });
+ nudWorld.Minimum = new decimal(new int[] { 1, 0, 0, 0 });
+ nudWorld.Name = "nudWorld";
+ nudWorld.Size = new Size(47, 27);
+ nudWorld.TabIndex = 26;
+ nudWorld.TextAlign = HorizontalAlignment.Center;
+ nudWorld.Value = new decimal(new int[] { 1, 0, 0, 0 });
+ nudWorld.ValueChanged += Item_ValueChanged;
+ //
+ // lblWorld
+ //
+ lblWorld.AutoSize = true;
+ lblWorld.Location = new Point(147, 106);
+ lblWorld.Margin = new Padding(4, 0, 4, 0);
+ lblWorld.Name = "lblWorld";
+ lblWorld.Size = new Size(49, 20);
+ lblWorld.TabIndex = 25;
+ lblWorld.Text = "World";
+ //
+ // lblDestPage
+ //
+ lblDestPage.AutoSize = true;
+ lblDestPage.Location = new Point(8, 106);
+ lblDestPage.Margin = new Padding(4, 0, 4, 0);
+ lblDestPage.Name = "lblDestPage";
+ lblDestPage.Size = new Size(75, 20);
+ lblDestPage.TabIndex = 24;
+ lblDestPage.Text = "Dest Page";
+ //
+ // nudDestPage
+ //
+ nudDestPage.Location = new Point(92, 103);
+ nudDestPage.Margin = new Padding(4, 5, 4, 5);
+ nudDestPage.Maximum = new decimal(new int[] { 31, 0, 0, 0 });
+ nudDestPage.Name = "nudDestPage";
+ nudDestPage.Size = new Size(47, 27);
+ nudDestPage.TabIndex = 23;
+ nudDestPage.TextAlign = HorizontalAlignment.Center;
+ nudDestPage.Value = new decimal(new int[] { 1, 0, 0, 0 });
+ nudDestPage.ValueChanged += Item_ValueChanged;
+ //
+ // chkHardFlag
+ //
+ chkHardFlag.AutoSize = true;
+ chkHardFlag.Location = new Point(363, 23);
+ chkHardFlag.Margin = new Padding(4, 5, 4, 5);
+ chkHardFlag.Name = "chkHardFlag";
+ chkHardFlag.Size = new Size(96, 24);
+ chkHardFlag.TabIndex = 22;
+ chkHardFlag.Text = "Hard Flag";
+ chkHardFlag.UseVisualStyleBackColor = true;
+ chkHardFlag.CheckedChanged += Item_ValueChanged;
+ //
+ // lblObject
+ //
+ lblObject.AutoSize = true;
+ lblObject.Location = new Point(8, 66);
+ lblObject.Margin = new Padding(4, 0, 4, 0);
+ lblObject.Name = "lblObject";
+ lblObject.Size = new Size(53, 20);
+ lblObject.TabIndex = 6;
+ lblObject.Text = "Object";
+ //
+ // cbxAreaSpriteCode
+ //
+ cbxAreaSpriteCode.DropDownStyle = ComboBoxStyle.DropDownList;
+ cbxAreaSpriteCode.FormattingEnabled = true;
+ cbxAreaSpriteCode.Location = new Point(67, 62);
+ cbxAreaSpriteCode.Margin = new Padding(4, 5, 4, 5);
+ cbxAreaSpriteCode.Name = "cbxAreaSpriteCode";
+ cbxAreaSpriteCode.Size = new Size(391, 28);
+ cbxAreaSpriteCode.TabIndex = 5;
+ cbxAreaSpriteCode.SelectedIndexChanged += AreaSpriteCode_SelectedIndexChanged;
+ //
+ // lblY
+ //
+ lblY.AutoSize = true;
+ lblY.Location = new Point(227, 25);
+ lblY.Margin = new Padding(4, 0, 4, 0);
+ lblY.Name = "lblY";
+ lblY.Size = new Size(45, 20);
+ lblY.TabIndex = 3;
+ lblY.Text = "Y pos";
+ //
+ // lblX
+ //
+ lblX.AutoSize = true;
+ lblX.Location = new Point(8, 25);
+ lblX.Margin = new Padding(4, 0, 4, 0);
+ lblX.Name = "lblX";
+ lblX.Size = new Size(46, 20);
+ lblX.TabIndex = 2;
+ lblX.Text = "X pos";
+ //
+ // nudY
+ //
+ nudY.Location = new Point(280, 22);
+ nudY.Margin = new Padding(4, 5, 4, 5);
+ nudY.Maximum = new decimal(new int[] { 11, 0, 0, 0 });
+ nudY.Name = "nudY";
+ nudY.Size = new Size(47, 27);
+ nudY.TabIndex = 1;
+ nudY.TextAlign = HorizontalAlignment.Center;
+ nudY.ValueChanged += Item_ValueChanged;
+ //
+ // nudX
+ //
+ nudX.Location = new Point(67, 22);
+ nudX.Margin = new Padding(4, 5, 4, 5);
+ nudX.Maximum = new decimal(new int[] { 15, 0, 0, 0 });
+ nudX.Name = "nudX";
+ nudX.Size = new Size(47, 27);
+ nudX.TabIndex = 0;
+ nudX.TextAlign = HorizontalAlignment.Center;
+ nudX.ValueChanged += Item_ValueChanged;
+ //
+ // SpriteEditorForm
+ //
+ AcceptButton = btnOK;
+ AutoScaleDimensions = new SizeF(8F, 20F);
+ AutoScaleMode = AutoScaleMode.Font;
+ CancelButton = btnCancel;
+ ClientSize = new Size(499, 280);
+ Controls.Add(groupBox1);
+ Controls.Add(gbxBinary);
+ Controls.Add(btnCancel);
+ Controls.Add(btnOK);
+ Margin = new Padding(4, 5, 4, 5);
+ MaximizeBox = false;
+ MinimizeBox = false;
+ Name = "SpriteEditorForm";
+ ShowIcon = false;
+ ShowInTaskbar = false;
+ Text = "Sprite Editor";
+ gbxBinary.ResumeLayout(false);
+ gbxBinary.PerformLayout();
+ groupBox1.ResumeLayout(false);
+ groupBox1.PerformLayout();
+ ((System.ComponentModel.ISupportInitialize)nudPage).EndInit();
+ ((System.ComponentModel.ISupportInitialize)nudWorld).EndInit();
+ ((System.ComponentModel.ISupportInitialize)nudDestPage).EndInit();
+ ((System.ComponentModel.ISupportInitialize)nudY).EndInit();
+ ((System.ComponentModel.ISupportInitialize)nudX).EndInit();
+ ResumeLayout(false);
+ }
+
+ #endregion
+
+ private System.Windows.Forms.GroupBox gbxBinary;
+ private System.Windows.Forms.TextBox tbxManualInput;
+ private System.Windows.Forms.CheckBox chkUseManualInput;
+ private System.Windows.Forms.Button btnCancel;
+ private System.Windows.Forms.Button btnOK;
+ private System.Windows.Forms.GroupBox groupBox1;
+ private System.Windows.Forms.Label lblObject;
+ private System.Windows.Forms.ComboBox cbxAreaSpriteCode;
+ private System.Windows.Forms.Label lblY;
+ private System.Windows.Forms.Label lblX;
+ private System.Windows.Forms.NumericUpDown nudY;
+ private System.Windows.Forms.NumericUpDown nudX;
+ private System.Windows.Forms.TextBox tbxAreaNumber;
+ private System.Windows.Forms.Label lblAreaNumber;
+ private System.Windows.Forms.NumericUpDown nudWorld;
+ private System.Windows.Forms.Label lblWorld;
+ private System.Windows.Forms.Label lblDestPage;
+ private System.Windows.Forms.NumericUpDown nudDestPage;
+ private System.Windows.Forms.CheckBox chkHardFlag;
+ private System.Windows.Forms.Label lblPage;
+ private System.Windows.Forms.NumericUpDown nudPage;
+ }
+}
diff --git a/src/Brutario.Win/Dialogs/BaseForms/SpriteEditorForm.cs b/src/Brutario.Win/Dialogs/BaseForms/SpriteEditorForm.cs
new file mode 100644
index 0000000..ee558ff
--- /dev/null
+++ b/src/Brutario.Win/Dialogs/BaseForms/SpriteEditorForm.cs
@@ -0,0 +1,414 @@
+//
+// Copyright (c) 2022 spel werdz rite. All rights reserved. Licensed under GNU Affero
+// General Public License. See LICENSE in project root for full license
+// information, or visit https://www.gnu.org/licenses/#AGPL
+//
+
+namespace Brutario.Win.Dialogs.BaseForms;
+
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Windows.Forms;
+
+using Core;
+
+using Maseya.Smas.Smb1;
+using Maseya.Smas.Smb1.AreaData.SpriteData;
+
+public partial class SpriteEditorForm : Form
+{
+ public SpriteEditorForm()
+ {
+ InitializeComponent();
+
+ Codes = new List();
+ EnumIndexes = new Dictionary();
+ foreach (var obj in Enum.GetValues(typeof(AreaSpriteCode)))
+ {
+ var code = (AreaSpriteCode)obj;
+ if (code == AreaSpriteCode.ScreenJump)
+ {
+ continue;
+ }
+
+ EnumIndexes.Add(code, Codes.Count);
+ Codes.Add(code);
+ _ = cbxAreaSpriteCode.Items.Add(code.BaseName());
+ }
+ }
+
+ public event EventHandler? AreaSpriteCommandChanged;
+
+ public UIAreaSpriteCommand AreaSpriteCommand
+ {
+ get
+ {
+ return UseManualInput ? BinaryCommand : UICommand;
+ }
+
+ set
+ {
+ BinaryCommand = UICommand = value;
+ if (IsValidInput)
+ {
+ AreaSpriteCommandChanged?.Invoke(this, EventArgs.Empty);
+ }
+ }
+ }
+
+ private bool IsValidInput
+ {
+ get
+ {
+ return btnOK.Enabled;
+ }
+
+ set
+ {
+ btnOK.Enabled = value;
+ }
+ }
+
+ private bool UseManualInput
+ {
+ get
+ {
+ return chkUseManualInput.Checked;
+ }
+
+ set
+ {
+ chkUseManualInput.Checked = value;
+ }
+ }
+
+ ///
+ /// Returns true when the UICommand is being updated by its set accessor.
+ ///
+ private bool SettingUICommand
+ {
+ get;
+ set;
+ }
+
+ private int XPos
+ {
+ get
+ {
+ return (int)nudX.Value;
+ }
+
+ set
+ {
+ nudX.Value = value;
+ }
+ }
+
+ private int Page
+ {
+ get
+ {
+ return (int)nudPage.Value;
+ }
+
+ set
+ {
+ nudPage.Value = value;
+ }
+ }
+
+ private int YPos
+ {
+ get
+ {
+ var y = (int)AreaSpriteCode >> 8;
+ return y < 0x0D ? (int)nudY.Value : y;
+ }
+
+ set
+ {
+ if (value <= nudY.Maximum)
+ {
+ nudY.Value = value;
+ }
+ }
+ }
+
+ private AreaSpriteCode AreaSpriteCode
+ {
+ get
+ {
+ return Codes[
+ cbxAreaSpriteCode.SelectedIndex >= 0
+ ? cbxAreaSpriteCode.SelectedIndex
+ : 0];
+ }
+
+ set
+ {
+ cbxAreaSpriteCode.SelectedIndex = EnumIndexes.TryGetValue(
+ value,
+ out var index) ? index : -1;
+ }
+ }
+
+ private int DestPage
+ {
+ get
+ {
+ return nudDestPage.Enabled ? (int)nudDestPage.Value : 1;
+ }
+
+ set
+ {
+ if (value <= nudDestPage.Maximum)
+ {
+ nudDestPage.Value = value;
+ }
+ }
+ }
+
+ private int World
+ {
+ get
+ {
+ return nudWorld.Enabled ? (int)nudWorld.Value : 1;
+ }
+
+ set
+ {
+ if (value <= nudWorld.Maximum)
+ {
+ nudWorld.Value = value;
+ }
+ }
+ }
+
+ private bool HardFlag
+ {
+ get
+ {
+ return chkHardFlag.Enabled && chkHardFlag.Checked;
+ }
+
+ set
+ {
+ chkHardFlag.Checked = value;
+ }
+ }
+
+ private int AreaNumber
+ {
+ get
+ {
+ _ = TryGetAreaNumber(tbxAreaNumber.Text, out var result);
+ return tbxAreaNumber.Enabled ? result : 0;
+ }
+
+ set
+ {
+ tbxAreaNumber.Text = $"{value & 0xFF:X2}";
+ }
+ }
+
+ private UIAreaSpriteCommand UICommand
+ {
+ get
+ {
+ var result = default(AreaSpriteCommand);
+ result.Value1 |= (byte)(XPos << 4);
+ switch (AreaSpriteCode)
+ {
+ case AreaSpriteCode.AreaPointer:
+ result.Value1 |= 0x0E;
+ result.Value2 |= (byte)(AreaNumber & 0x7F);
+ result.Value3 |= (byte)((World - 1) << 5);
+ result.Value3 |= (byte)(DestPage & 0x1F);
+ break;
+
+ default:
+ result.Value1 |= (byte)YPos;
+ result.Value2 |= (byte)((int)AreaSpriteCode & 0x3F);
+ break;
+ }
+
+ result.HardWorldFlag |= HardFlag;
+
+ return new UIAreaSpriteCommand(result, Page);
+ }
+
+ set
+ {
+ SettingUICommand = true;
+ var command = value.Command;
+ UpdateEnabledControls(command);
+
+ XPos = command.X;
+ AreaSpriteCode = command.Code;
+ switch (command.Code)
+ {
+ case AreaSpriteCode.AreaPointer:
+ Page = 1 + (command.Value3 & 0x1F);
+ World = 1 + command.WorldLimit;
+ AreaNumber = command.AreaNumber;
+ break;
+
+ default:
+ YPos = command.Y;
+ HardFlag = command.HardWorldFlag;
+ break;
+ }
+
+ Page = value.Page;
+ SettingUICommand = false;
+ }
+ }
+
+ private UIAreaSpriteCommand BinaryCommand
+ {
+ get
+ {
+ _ = TryGetCommand(tbxManualInput.Text, out var result);
+ return result;
+ }
+
+ set
+ {
+ tbxManualInput.Text = value.HexString;
+ }
+ }
+
+ private List Codes
+ {
+ get;
+ }
+
+ private Dictionary EnumIndexes
+ {
+ get;
+ }
+
+ private static bool TryGetAreaNumber(string text, out byte result)
+ {
+ if (text.Length != 2)
+ {
+ result = 0;
+ return false;
+ }
+
+ return Byte.TryParse(
+ text,
+ NumberStyles.HexNumber,
+ CultureInfo.CurrentUICulture,
+ out result);
+ }
+
+ private static bool TryGetCommand(string text, out UIAreaSpriteCommand command)
+ {
+ var tokens = text.Split(' ');
+ if (tokens.Length is not 4 and not 3)
+ {
+ command = default;
+ return false;
+ }
+
+ var bytes = new byte[4];
+ for (var i = 0; i < tokens.Length; i++)
+ {
+ if (tokens[i].Length != 2)
+ {
+ command = default;
+ return false;
+ }
+
+ if (!Byte.TryParse(
+ tokens[i],
+ NumberStyles.HexNumber,
+ CultureInfo.CurrentUICulture,
+ out bytes[i]))
+ {
+ command = default;
+ return false;
+ }
+ }
+
+ var result = new AreaSpriteCommand(bytes[1], bytes[2], bytes[3]);
+ if (!result.IsValid || bytes[0] >= 0x20
+ || result.Code == AreaSpriteCode.ScreenJump)
+ {
+ command = default;
+ return false;
+ }
+
+ command = new UIAreaSpriteCommand(result, bytes[0]);
+ return true;
+ }
+
+ private void UpdateEnabledControls(AreaSpriteCommand value)
+ {
+ lblY.Enabled =
+ nudY.Enabled =
+ chkHardFlag.Enabled = value.Y <= 0x0D;
+
+ lblDestPage.Enabled =
+ nudDestPage.Enabled = value.Y > 0x0D;
+
+ lblWorld.Enabled =
+ nudWorld.Enabled =
+ lblAreaNumber.Enabled =
+ tbxAreaNumber.Enabled = value.Code == AreaSpriteCode.AreaPointer;
+ }
+
+ private void UpdateValidInput()
+ {
+ // If we're using the list and check boxes, then the input is always valid by
+ // their restraints. Otherwise, if we're entering the value manually, then we
+ // must check that text is valid.
+ IsValidInput =
+ !UseManualInput || TryGetCommand(tbxManualInput.Text, out var _);
+ }
+
+ private void AreaNumber_TextChanged(object? sender, EventArgs e)
+ {
+ IsValidInput = !tbxAreaNumber.Enabled
+ || TryGetAreaNumber(tbxAreaNumber.Text, out var _);
+ }
+
+ private void ManualInput_TextChanged(object? sender, EventArgs e)
+ {
+ UpdateValidInput();
+ if (!SettingUICommand && btnOK.Enabled && UICommand != BinaryCommand)
+ {
+ UICommand = BinaryCommand;
+ AreaSpriteCommandChanged?.Invoke(this, EventArgs.Empty);
+ }
+ }
+
+ private void AreaSpriteCode_SelectedIndexChanged(object? sender, EventArgs e)
+ {
+ if (cbxAreaSpriteCode.SelectedIndex == -1 || SettingUICommand)
+ {
+ return;
+ }
+
+ UpdateEnabledControls(UICommand.Command);
+ BinaryCommand = UICommand;
+ AreaSpriteCommandChanged?.Invoke(this, EventArgs.Empty);
+ }
+
+ private void Item_ValueChanged(object? sender, EventArgs e)
+ {
+ var control = sender as Control;
+ if (control!.Enabled && !SettingUICommand && BinaryCommand != UICommand)
+ {
+ BinaryCommand = UICommand;
+ AreaSpriteCommandChanged?.Invoke(this, EventArgs.Empty);
+ }
+ }
+
+ private void UseManualInput_CheckedChanged(object? sender, EventArgs e)
+ {
+ UpdateValidInput();
+ }
+}
diff --git a/src/Brutario.Win/Dialogs/BaseForms/SpriteEditorForm.resx b/src/Brutario.Win/Dialogs/BaseForms/SpriteEditorForm.resx
new file mode 100644
index 0000000..8b2ff64
--- /dev/null
+++ b/src/Brutario.Win/Dialogs/BaseForms/SpriteEditorForm.resx
@@ -0,0 +1,120 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
\ No newline at end of file
diff --git a/src/Brutario.Win/Dialogs/HeaderEditorDialog.cs b/src/Brutario.Win/Dialogs/HeaderEditorDialog.cs
new file mode 100644
index 0000000..34e8642
--- /dev/null
+++ b/src/Brutario.Win/Dialogs/HeaderEditorDialog.cs
@@ -0,0 +1,69 @@
+//
+// Copyright (c) 2022 spel werdz rite. All rights reserved. Licensed under GNU Affero
+// General Public License. See LICENSE in project root for full license
+// information, or visit https://www.gnu.org/licenses/#AGPL
+//
+
+namespace Brutario.Win.Dialogs;
+
+using System;
+using System.ComponentModel;
+using System.Windows.Forms;
+
+using BaseForms;
+
+using Controls;
+
+using Maseya.Smas.Smb1.AreaData.HeaderData;
+
+using static System.ComponentModel.DesignerSerializationVisibility;
+
+public sealed class HeaderEditorDialog : DialogProxy
+{
+ public HeaderEditorDialog()
+ {
+ HeaderEditorForm = new HeaderEditorForm();
+ HeaderEditorForm.AreaHeaderChanged += HeaderEditorForm_AreaHeaderChanged;
+ }
+
+ public HeaderEditorDialog(IContainer container)
+ : this()
+ {
+ container.Add(this);
+ }
+
+ public event EventHandler? AreaHeaderChanged;
+
+ [Browsable(false)]
+ [DesignerSerializationVisibility(Hidden)]
+ public AreaHeader AreaHeader
+ {
+ get
+ {
+ return HeaderEditorForm.AreaHeader;
+ }
+
+ set
+ {
+ HeaderEditorForm.AreaHeader = value;
+ }
+ }
+
+ protected override Form BaseForm
+ {
+ get
+ {
+ return HeaderEditorForm;
+ }
+ }
+
+ private HeaderEditorForm HeaderEditorForm
+ {
+ get;
+ }
+
+ private void HeaderEditorForm_AreaHeaderChanged(object? sender, EventArgs e)
+ {
+ AreaHeaderChanged?.Invoke(this, EventArgs.Empty);
+ }
+}
diff --git a/src/Brutario.Win/Dialogs/HeaderEditorDialog.resx b/src/Brutario.Win/Dialogs/HeaderEditorDialog.resx
new file mode 100644
index 0000000..1af7de1
--- /dev/null
+++ b/src/Brutario.Win/Dialogs/HeaderEditorDialog.resx
@@ -0,0 +1,120 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
\ No newline at end of file
diff --git a/src/Brutario.Win/Dialogs/ObjectEditorDialog.cs b/src/Brutario.Win/Dialogs/ObjectEditorDialog.cs
new file mode 100644
index 0000000..c68329d
--- /dev/null
+++ b/src/Brutario.Win/Dialogs/ObjectEditorDialog.cs
@@ -0,0 +1,96 @@
+//
+// Copyright (c) 2022 spel werdz rite. All rights reserved. Licensed under GNU Affero
+// General Public License. See LICENSE in project root for full license
+// information, or visit https://www.gnu.org/licenses/#AGPL
+//
+
+namespace Brutario.Win.Dialogs;
+
+using System;
+using System.ComponentModel;
+using System.Windows.Forms;
+
+using BaseForms;
+
+using Controls;
+
+using Core;
+
+using Maseya.Smas.Smb1.AreaData.ObjectData;
+
+using static System.ComponentModel.DesignerSerializationVisibility;
+
+public sealed class ObjectEditorDialog : DialogProxy
+{
+ public ObjectEditorDialog()
+ {
+ ObjectEditorForm = new ObjectEditorForm();
+ ObjectEditorForm.AreaObjectCommandChanged +=
+ ObjectEditorForm_AreaObjectCommandChanged;
+ ObjectEditorForm.AreaPlatformTypeChanged +=
+ ObjectEditorForm_AreaPlatformTypeChanged;
+ }
+
+ public ObjectEditorDialog(IContainer container)
+ : this()
+ {
+ container.Add(this);
+ }
+
+ public event EventHandler? AreaPlatformTypeChanged;
+
+ public event EventHandler? AreaObjectCommandChanged;
+
+ [Browsable(false)]
+ [DesignerSerializationVisibility(Hidden)]
+ public AreaPlatformType AreaPlatformType
+ {
+ get
+ {
+ return ObjectEditorForm.AreaPlatformType;
+ }
+
+ set
+ {
+ ObjectEditorForm.AreaPlatformType = value;
+ }
+ }
+
+ [Browsable(false)]
+ [DesignerSerializationVisibility(Hidden)]
+ public UIAreaObjectCommand AreaObjectCommand
+ {
+ get
+ {
+ return ObjectEditorForm.AreaObjectCommand;
+ }
+
+ set
+ {
+ ObjectEditorForm.AreaObjectCommand = value;
+ }
+ }
+
+ protected override Form BaseForm
+ {
+ get
+ {
+ return ObjectEditorForm;
+ }
+ }
+
+ private ObjectEditorForm ObjectEditorForm
+ {
+ get;
+ }
+
+ private void ObjectEditorForm_AreaPlatformTypeChanged(object? sender, EventArgs e)
+ {
+ AreaPlatformTypeChanged?.Invoke(this, EventArgs.Empty);
+ }
+
+ private void ObjectEditorForm_AreaObjectCommandChanged(object? sender, EventArgs e)
+ {
+ AreaObjectCommandChanged?.Invoke(this, EventArgs.Empty);
+ }
+}
diff --git a/src/Brutario.Win/Dialogs/ObjectListDialog.cs b/src/Brutario.Win/Dialogs/ObjectListDialog.cs
new file mode 100644
index 0000000..329f4e9
--- /dev/null
+++ b/src/Brutario.Win/Dialogs/ObjectListDialog.cs
@@ -0,0 +1,196 @@
+//
+// Copyright (c) 2022 spel werdz rite. All rights reserved. Licensed under GNU Affero
+// General Public License. See LICENSE in project root for full license
+// information, or visit https://www.gnu.org/licenses/#AGPL
+//
+
+namespace Brutario.Win.Dialogs;
+
+using System;
+using System.ComponentModel;
+using System.Windows.Forms;
+
+using BaseForms;
+
+using Controls;
+
+using Maseya.Smas.Smb1.AreaData.ObjectData;
+
+using static System.ComponentModel.DesignerSerializationVisibility;
+using static BaseForms.ObjectListForm;
+
+public class ObjectListDialog : DialogProxy
+{
+ public ObjectListDialog()
+ : base()
+ {
+ ObjectListWindow = new ObjectListForm();
+ ObjectListWindow.AreaPlatformTypeChanged += ObjectListWindow_AreaPlatformTypeChanged;
+ ObjectListWindow.SelectedIndexChanged += ObjectListWindow_SelectedIndexChanged;
+ ObjectListWindow.EditItem += ObjectListWindow_EditItem;
+ ObjectListWindow.AddItem_Click += ObjectListWindow_AddItem_Click;
+ ObjectListWindow.DeleteItem_Click += ObjectListWindow_DeleteItem_Click;
+ ObjectListWindow.ClearItems_Click += ObjectListWindow_ClearItems_Click;
+ ObjectListWindow.FormClosing += ObjectListWindow_FormClosing;
+ ObjectListWindow.VisibleChanged += ObjectListWindow_VisibleChanged;
+ }
+
+ public ObjectListDialog(IContainer container)
+ : this()
+ {
+ container.Add(this);
+ }
+
+ public event EventHandler? AreaPlatformTypeChanged;
+
+ public event EventHandler? SelectedIndexChanged;
+
+ public event EventHandler? EditItem;
+
+ public event EventHandler? AddItem_Click;
+
+ public event EventHandler? DeleteItem_Click;
+
+ public event EventHandler? ClearItems_Click;
+
+ public event EventHandler? MoveItemDown_Click;
+
+ public event EventHandler? MoveItemUp_Click;
+
+ public event EventHandler? VisibleChanged;
+
+ [Browsable(false)]
+ [DesignerSerializationVisibility(Hidden)]
+ public AreaPlatformType AreaPlatformType
+ {
+ get
+ {
+ return ObjectListWindow.AreaPlatformType;
+ }
+
+ set
+ {
+ ObjectListWindow.AreaPlatformType = value;
+ }
+ }
+
+ [Browsable(false)]
+ [DesignerSerializationVisibility(Hidden)]
+ public int SelectedIndex
+ {
+ get
+ {
+ return ObjectListWindow.SelectedIndex;
+ }
+
+ set
+ {
+ ObjectListWindow.SelectedIndex = value;
+ }
+ }
+
+ [Browsable(false)]
+ [DesignerSerializationVisibility(Hidden)]
+ public ItemCollection Items
+ {
+ get
+ {
+ return ObjectListWindow.Items;
+ }
+ }
+
+ [Browsable(false)]
+ [DesignerSerializationVisibility(Hidden)]
+ public bool Visible
+ {
+ get
+ {
+ return ObjectListWindow.Visible;
+ }
+
+ set
+ {
+ ObjectListWindow.Visible = value;
+ }
+ }
+
+ public Form Owner
+ {
+ get
+ {
+ return ObjectListWindow.Owner;
+ }
+
+ set
+ {
+ ObjectListWindow.Owner = value;
+ }
+ }
+
+ protected override Form BaseForm
+ {
+ get
+ {
+ return ObjectListWindow;
+ }
+ }
+
+ private ObjectListForm ObjectListWindow
+ {
+ get;
+ }
+
+ private void ObjectListWindow_AreaPlatformTypeChanged(object? sender, EventArgs e)
+ {
+ AreaPlatformTypeChanged?.Invoke(this, EventArgs.Empty);
+ }
+
+ private void ObjectListWindow_SelectedIndexChanged(object? sender, EventArgs e)
+ {
+ SelectedIndexChanged?.Invoke(this, EventArgs.Empty);
+ }
+
+ private void ObjectListWindow_EditItem(object? sender, EventArgs e)
+ {
+ EditItem?.Invoke(this, EventArgs.Empty);
+ }
+
+ private void ObjectListWindow_AddItem_Click(object? sender, EventArgs e)
+ {
+ AddItem_Click?.Invoke(this, EventArgs.Empty);
+ }
+
+ private void ObjectListWindow_DeleteItem_Click(object? sender, EventArgs e)
+ {
+ DeleteItem_Click?.Invoke(this, EventArgs.Empty);
+ }
+
+ private void ObjectListWindow_ClearItems_Click(object? sender, EventArgs e)
+ {
+ ClearItems_Click?.Invoke(this, EventArgs.Empty);
+ }
+
+ private void ObjectListWindow_MoveItemDown_Click(object? sender, EventArgs e)
+ {
+ MoveItemDown_Click?.Invoke(this, EventArgs.Empty);
+ }
+
+ private void ObjectListWindow_MoveItemUp_Click(object? sender, EventArgs e)
+ {
+ MoveItemUp_Click?.Invoke(this, EventArgs.Empty);
+ }
+
+ private void ObjectListWindow_VisibleChanged(object? sender, EventArgs e)
+ {
+ VisibleChanged?.Invoke(this, EventArgs.Empty);
+ }
+
+ private void ObjectListWindow_FormClosing(object? sender, FormClosingEventArgs e)
+ {
+ if (Owner != null && e.CloseReason == CloseReason.UserClosing)
+ {
+ ObjectListWindow.Visible = false;
+ e.Cancel = true;
+ }
+ }
+}
diff --git a/src/Brutario.Win/Dialogs/SpriteEditorDialog.cs b/src/Brutario.Win/Dialogs/SpriteEditorDialog.cs
new file mode 100644
index 0000000..35622db
--- /dev/null
+++ b/src/Brutario.Win/Dialogs/SpriteEditorDialog.cs
@@ -0,0 +1,70 @@
+//
+// Copyright (c) 2022 spel werdz rite. All rights reserved. Licensed under GNU Affero
+// General Public License. See LICENSE in project root for full license
+// information, or visit https://www.gnu.org/licenses/#AGPL
+//
+
+namespace Brutario.Win.Dialogs;
+
+using System;
+using System.ComponentModel;
+using System.Windows.Forms;
+
+using BaseForms;
+
+using Controls;
+
+using Core;
+
+using static System.ComponentModel.DesignerSerializationVisibility;
+
+public class SpriteEditorDialog : DialogProxy
+{
+ public SpriteEditorDialog()
+ {
+ SpriteEditorForm = new SpriteEditorForm();
+ SpriteEditorForm.AreaSpriteCommandChanged +=
+ SpriteEditorForm_AreaSpriteCommandChanged;
+ }
+
+ public SpriteEditorDialog(IContainer container)
+ : this()
+ {
+ container.Add(this);
+ }
+
+ public event EventHandler? AreaSpriteCommandChanged;
+
+ [Browsable(false)]
+ [DesignerSerializationVisibility(Hidden)]
+ public UIAreaSpriteCommand AreaSpriteCommand
+ {
+ get
+ {
+ return SpriteEditorForm.AreaSpriteCommand;
+ }
+
+ set
+ {
+ SpriteEditorForm.AreaSpriteCommand = value;
+ }
+ }
+
+ protected override Form BaseForm
+ {
+ get
+ {
+ return SpriteEditorForm;
+ }
+ }
+
+ private SpriteEditorForm SpriteEditorForm
+ {
+ get;
+ }
+
+ private void SpriteEditorForm_AreaSpriteCommandChanged(object? sender, EventArgs e)
+ {
+ AreaSpriteCommandChanged?.Invoke(this, EventArgs.Empty);
+ }
+}
diff --git a/src/Brutario.Win/MainForm.Designer.cs b/src/Brutario.Win/MainForm.Designer.cs
new file mode 100644
index 0000000..931fe34
--- /dev/null
+++ b/src/Brutario.Win/MainForm.Designer.cs
@@ -0,0 +1,928 @@
+//
+// Copyright (c) 2022 spel werdz rite. All rights reserved. Licensed
+// under GNU Affero General Public License. See LICENSE in project
+// root for full license information, or visit
+// https://www.gnu.org/licenses/#AGPL
+//
+
+namespace Brutario.Win
+{
+ partial class MainForm
+ {
+ ///
+ /// Required designer variable.
+ ///
+ private System.ComponentModel.IContainer components = null;
+
+ ///
+ /// Clean up any resources being used.
+ ///
+ /// true if managed resources should be disposed; otherwise, false.
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing && (components != null))
+ {
+ components.Dispose();
+ }
+ base.Dispose(disposing);
+ }
+
+ #region Windows Form Designer generated code
+
+ ///
+ /// Required method for Designer support - do not modify
+ /// the contents of this method with the code editor.
+ ///
+ private void InitializeComponent()
+ {
+ components = new System.ComponentModel.Container();
+ var resources = new System.ComponentModel.ComponentResourceManager(typeof(MainForm));
+ mnuMain = new MenuStrip();
+ tsmFile = new ToolStripMenuItem();
+ tsmOpen = new ToolStripMenuItem();
+ tsmSave = new ToolStripMenuItem();
+ tsmSaveAs = new ToolStripMenuItem();
+ toolStripSeparator1 = new ToolStripSeparator();
+ tsmClose = new ToolStripMenuItem();
+ toolStripSeparator10 = new ToolStripSeparator();
+ tsmExit = new ToolStripMenuItem();
+ tsmEdit = new ToolStripMenuItem();
+ tsmUndo = new ToolStripMenuItem();
+ tsmRedo = new ToolStripMenuItem();
+ toolStripSeparator7 = new ToolStripSeparator();
+ tsmCut = new ToolStripMenuItem();
+ tsmCopy = new ToolStripMenuItem();
+ tsmPaste = new ToolStripMenuItem();
+ toolStripSeparator8 = new ToolStripSeparator();
+ tsmAddItem = new ToolStripMenuItem();
+ tsmRemoveItem = new ToolStripMenuItem();
+ tsmDeleteAll = new ToolStripMenuItem();
+ tsmLevel = new ToolStripMenuItem();
+ tsmLoadArea = new ToolStripMenuItem();
+ tsmExportTileData = new ToolStripMenuItem();
+ tsmEditHeader = new ToolStripMenuItem();
+ tsmSpriteMode = new ToolStripMenuItem();
+ tsmView = new ToolStripMenuItem();
+ tsmPlayerState = new ToolStripMenuItem();
+ tsmSmall = new ToolStripMenuItem();
+ tsmBig = new ToolStripMenuItem();
+ tsmFire = new ToolStripMenuItem();
+ tsmPlayer = new ToolStripMenuItem();
+ tsmMario = new ToolStripMenuItem();
+ tsmLuigi = new ToolStripMenuItem();
+ tsmViewObjectList = new ToolStripMenuItem();
+ tsmHelp = new ToolStripMenuItem();
+ tsmAutoSave = new ToolStripMenuItem();
+ toolStripSeparator12 = new ToolStripSeparator();
+ tsmSpecialThanks = new ToolStripMenuItem();
+ tsmAbout = new ToolStripMenuItem();
+ toolStrip = new ToolStrip();
+ tsbOpen = new ToolStripButton();
+ tsbSave = new ToolStripButton();
+ toolStripSeparator = new ToolStripSeparator();
+ tslJumpToArea = new ToolStripLabel();
+ ttbJumpToArea = new ToolStripTextBox();
+ tsbJumpToArea = new ToolStripButton();
+ toolStripSeparator4 = new ToolStripSeparator();
+ tsbLoadAreaByLevel = new ToolStripButton();
+ toolStripSeparator3 = new ToolStripSeparator();
+ tsbUndo = new ToolStripButton();
+ tsbRedo = new ToolStripButton();
+ toolStripSeparator2 = new ToolStripSeparator();
+ tsbCut = new ToolStripButton();
+ tsbCopy = new ToolStripButton();
+ tsbPaste = new ToolStripButton();
+ toolStripSeparator5 = new ToolStripSeparator();
+ tsbAddItem = new ToolStripButton();
+ tsbRemoveItem = new ToolStripButton();
+ tsbDeleteAll = new ToolStripButton();
+ toolStripSeparator6 = new ToolStripSeparator();
+ tsbSpriteMode = new ToolStripButton();
+ toolStripSeparator9 = new ToolStripSeparator();
+ tsbSpecialThanks = new ToolStripButton();
+ tsbHelp = new ToolStripButton();
+ hsbStartX = new HScrollBar();
+ cmsMain = new ContextMenuStrip(components);
+ cmiCut = new ToolStripMenuItem();
+ cmiCopy = new ToolStripMenuItem();
+ cmiPaste = new ToolStripMenuItem();
+ toolStripSeparator11 = new ToolStripSeparator();
+ cmiAddItem = new ToolStripMenuItem();
+ cmiRemoveItem = new ToolStripMenuItem();
+ cmiDeleteAll = new ToolStripMenuItem();
+ areaControl = new Controls.DesignControl();
+ headerEditor = new Views.HeaderEditor(components);
+ objectEditor = new Views.ObjectEditor(components);
+ spriteEditor = new Views.SpriteEditor(components);
+ saveOnClosePrompt = new Views.SaveOnClosePrompt(components);
+ exceptionHelper = new Views.ExceptionView(components);
+ saveFileNameSelector = new Views.SaveFileNameSelector(components);
+ openFileNameSelector = new Views.OpenFileNameSelector(components);
+ animationTimer = new System.Windows.Forms.Timer(components);
+ objectListView = new Views.ObjectListView(components);
+ autoSaveTimer = new System.Windows.Forms.Timer(components);
+ mnuMain.SuspendLayout();
+ toolStrip.SuspendLayout();
+ cmsMain.SuspendLayout();
+ SuspendLayout();
+ //
+ // mnuMain
+ //
+ mnuMain.ImageScalingSize = new Size(20, 20);
+ mnuMain.Items.AddRange(new ToolStripItem[] { tsmFile, tsmEdit, tsmLevel, tsmView, tsmHelp });
+ mnuMain.Location = new Point(0, 0);
+ mnuMain.Name = "mnuMain";
+ mnuMain.Padding = new Padding(8, 3, 0, 3);
+ mnuMain.Size = new Size(1030, 30);
+ mnuMain.TabIndex = 0;
+ mnuMain.Text = "menuStrip1";
+ //
+ // tsmFile
+ //
+ tsmFile.DropDownItems.AddRange(new ToolStripItem[] { tsmOpen, tsmSave, tsmSaveAs, toolStripSeparator1, tsmClose, toolStripSeparator10, tsmExit });
+ tsmFile.Name = "tsmFile";
+ tsmFile.Size = new Size(46, 24);
+ tsmFile.Text = "&File";
+ //
+ // tsmOpen
+ //
+ tsmOpen.Image = Properties.Resources.folder_open_solid;
+ tsmOpen.Name = "tsmOpen";
+ tsmOpen.ShortcutKeys = Keys.Control | Keys.O;
+ tsmOpen.Size = new Size(231, 26);
+ tsmOpen.Text = "&Open";
+ tsmOpen.Click += Open_Click;
+ //
+ // tsmSave
+ //
+ tsmSave.Enabled = false;
+ tsmSave.Image = Properties.Resources.floppy_disk_regular;
+ tsmSave.Name = "tsmSave";
+ tsmSave.ShortcutKeys = Keys.Control | Keys.S;
+ tsmSave.Size = new Size(231, 26);
+ tsmSave.Text = "&Save";
+ tsmSave.Click += Save_Click;
+ //
+ // tsmSaveAs
+ //
+ tsmSaveAs.Enabled = false;
+ tsmSaveAs.Name = "tsmSaveAs";
+ tsmSaveAs.ShortcutKeys = Keys.Control | Keys.Alt | Keys.S;
+ tsmSaveAs.Size = new Size(231, 26);
+ tsmSaveAs.Text = "Save &As...";
+ tsmSaveAs.Click += SaveAs_Click;
+ //
+ // toolStripSeparator1
+ //
+ toolStripSeparator1.Name = "toolStripSeparator1";
+ toolStripSeparator1.Size = new Size(228, 6);
+ //
+ // tsmClose
+ //
+ tsmClose.Enabled = false;
+ tsmClose.Name = "tsmClose";
+ tsmClose.ShortcutKeys = Keys.Control | Keys.F4;
+ tsmClose.Size = new Size(231, 26);
+ tsmClose.Text = "&Close";
+ tsmClose.Click += Close_Click;
+ //
+ // toolStripSeparator10
+ //
+ toolStripSeparator10.Name = "toolStripSeparator10";
+ toolStripSeparator10.Size = new Size(228, 6);
+ //
+ // tsmExit
+ //
+ tsmExit.Name = "tsmExit";
+ tsmExit.ShortcutKeys = Keys.Alt | Keys.F4;
+ tsmExit.Size = new Size(231, 26);
+ tsmExit.Text = "E&xit";
+ tsmExit.Click += Exit_Click;
+ //
+ // tsmEdit
+ //
+ tsmEdit.DropDownItems.AddRange(new ToolStripItem[] { tsmUndo, tsmRedo, toolStripSeparator7, tsmCut, tsmCopy, tsmPaste, toolStripSeparator8, tsmAddItem, tsmRemoveItem, tsmDeleteAll });
+ tsmEdit.Name = "tsmEdit";
+ tsmEdit.Size = new Size(49, 24);
+ tsmEdit.Text = "&Edit";
+ //
+ // tsmUndo
+ //
+ tsmUndo.Enabled = false;
+ tsmUndo.Image = Properties.Resources.rotate_left_solid;
+ tsmUndo.Name = "tsmUndo";
+ tsmUndo.ShortcutKeys = Keys.Control | Keys.Z;
+ tsmUndo.Size = new Size(263, 26);
+ tsmUndo.Text = "&Undo";
+ tsmUndo.Click += Undo_Click;
+ //
+ // tsmRedo
+ //
+ tsmRedo.Enabled = false;
+ tsmRedo.Image = Properties.Resources.rotate_right_solid;
+ tsmRedo.Name = "tsmRedo";
+ tsmRedo.ShortcutKeys = Keys.Control | Keys.Y;
+ tsmRedo.Size = new Size(263, 26);
+ tsmRedo.Text = "&Redo";
+ tsmRedo.Click += Redo_Click;
+ //
+ // toolStripSeparator7
+ //
+ toolStripSeparator7.Name = "toolStripSeparator7";
+ toolStripSeparator7.Size = new Size(260, 6);
+ //
+ // tsmCut
+ //
+ tsmCut.Enabled = false;
+ tsmCut.Image = Properties.Resources.scissors_solid;
+ tsmCut.Name = "tsmCut";
+ tsmCut.ShortcutKeys = Keys.Control | Keys.X;
+ tsmCut.Size = new Size(263, 26);
+ tsmCut.Text = "Cu&t";
+ tsmCut.Click += Cut_Click;
+ //
+ // tsmCopy
+ //
+ tsmCopy.Enabled = false;
+ tsmCopy.Image = Properties.Resources.copy_solid;
+ tsmCopy.Name = "tsmCopy";
+ tsmCopy.ShortcutKeys = Keys.Control | Keys.C;
+ tsmCopy.Size = new Size(263, 26);
+ tsmCopy.Text = "&Copy";
+ tsmCopy.Click += Copy_Click;
+ //
+ // tsmPaste
+ //
+ tsmPaste.Enabled = false;
+ tsmPaste.Image = Properties.Resources.paste_solid;
+ tsmPaste.Name = "tsmPaste";
+ tsmPaste.ShortcutKeys = Keys.Control | Keys.V;
+ tsmPaste.Size = new Size(263, 26);
+ tsmPaste.Text = "&Paste";
+ tsmPaste.Click += Paste_Click;
+ //
+ // toolStripSeparator8
+ //
+ toolStripSeparator8.Name = "toolStripSeparator8";
+ toolStripSeparator8.Size = new Size(260, 6);
+ //
+ // tsmAddItem
+ //
+ tsmAddItem.Enabled = false;
+ tsmAddItem.Image = Properties.Resources.plus_solid;
+ tsmAddItem.Name = "tsmAddItem";
+ tsmAddItem.ShortcutKeys = Keys.Insert;
+ tsmAddItem.Size = new Size(263, 26);
+ tsmAddItem.Text = "&Add Item";
+ tsmAddItem.Click += AddItem_Click;
+ //
+ // tsmRemoveItem
+ //
+ tsmRemoveItem.Enabled = false;
+ tsmRemoveItem.Image = Properties.Resources.minus_solid;
+ tsmRemoveItem.Name = "tsmRemoveItem";
+ tsmRemoveItem.ShortcutKeys = Keys.Delete;
+ tsmRemoveItem.Size = new Size(263, 26);
+ tsmRemoveItem.Text = "&Remove Item";
+ tsmRemoveItem.Click += RemoveItem_Click;
+ //
+ // tsmDeleteAll
+ //
+ tsmDeleteAll.Enabled = false;
+ tsmDeleteAll.Image = Properties.Resources.trash_solid;
+ tsmDeleteAll.Name = "tsmDeleteAll";
+ tsmDeleteAll.ShortcutKeys = Keys.Control | Keys.Shift | Keys.Delete;
+ tsmDeleteAll.Size = new Size(263, 26);
+ tsmDeleteAll.Text = "&Delete All";
+ tsmDeleteAll.Click += DeleteAll_Click;
+ //
+ // tsmLevel
+ //
+ tsmLevel.DropDownItems.AddRange(new ToolStripItem[] { tsmLoadArea, tsmExportTileData, tsmEditHeader, tsmSpriteMode });
+ tsmLevel.Name = "tsmLevel";
+ tsmLevel.Size = new Size(57, 24);
+ tsmLevel.Text = "Level";
+ //
+ // tsmLoadArea
+ //
+ tsmLoadArea.Enabled = false;
+ tsmLoadArea.Image = Properties.Resources.folder_tree_solid;
+ tsmLoadArea.Name = "tsmLoadArea";
+ tsmLoadArea.Size = new Size(229, 26);
+ tsmLoadArea.Text = "Load Area";
+ tsmLoadArea.Click += LoadArea_Click;
+ //
+ // tsmExportTileData
+ //
+ tsmExportTileData.Enabled = false;
+ tsmExportTileData.Name = "tsmExportTileData";
+ tsmExportTileData.Size = new Size(229, 26);
+ tsmExportTileData.Text = "Export Tile Data";
+ tsmExportTileData.Click += ExportTileData_Click;
+ //
+ // tsmEditHeader
+ //
+ tsmEditHeader.Enabled = false;
+ tsmEditHeader.Name = "tsmEditHeader";
+ tsmEditHeader.ShortcutKeys = Keys.Control | Keys.H;
+ tsmEditHeader.Size = new Size(229, 26);
+ tsmEditHeader.Text = "Edit Header";
+ tsmEditHeader.Click += EditHeader_Click;
+ //
+ // tsmSpriteMode
+ //
+ tsmSpriteMode.CheckOnClick = true;
+ tsmSpriteMode.Image = Properties.Resources.alien;
+ tsmSpriteMode.Name = "tsmSpriteMode";
+ tsmSpriteMode.ShortcutKeys = Keys.Control | Keys.M;
+ tsmSpriteMode.Size = new Size(229, 26);
+ tsmSpriteMode.Text = "Sprite Mode";
+ tsmSpriteMode.Click += SpriteMode_Click;
+ //
+ // tsmView
+ //
+ tsmView.DropDownItems.AddRange(new ToolStripItem[] { tsmPlayerState, tsmPlayer, tsmViewObjectList });
+ tsmView.Name = "tsmView";
+ tsmView.Size = new Size(55, 24);
+ tsmView.Text = "&View";
+ //
+ // tsmPlayerState
+ //
+ tsmPlayerState.DropDownItems.AddRange(new ToolStripItem[] { tsmSmall, tsmBig, tsmFire });
+ tsmPlayerState.Name = "tsmPlayerState";
+ tsmPlayerState.Size = new Size(318, 26);
+ tsmPlayerState.Text = "Player &State";
+ //
+ // tsmSmall
+ //
+ tsmSmall.Name = "tsmSmall";
+ tsmSmall.Size = new Size(129, 26);
+ tsmSmall.Text = "&Small";
+ tsmSmall.Click += PlayerState_Click;
+ //
+ // tsmBig
+ //
+ tsmBig.Name = "tsmBig";
+ tsmBig.Size = new Size(129, 26);
+ tsmBig.Text = "&Big";
+ tsmBig.Click += PlayerState_Click;
+ //
+ // tsmFire
+ //
+ tsmFire.Name = "tsmFire";
+ tsmFire.Size = new Size(129, 26);
+ tsmFire.Text = "&Fire";
+ tsmFire.Click += PlayerState_Click;
+ //
+ // tsmPlayer
+ //
+ tsmPlayer.DropDownItems.AddRange(new ToolStripItem[] { tsmMario, tsmLuigi });
+ tsmPlayer.Name = "tsmPlayer";
+ tsmPlayer.Size = new Size(318, 26);
+ tsmPlayer.Text = "&Player";
+ //
+ // tsmMario
+ //
+ tsmMario.Name = "tsmMario";
+ tsmMario.Size = new Size(131, 26);
+ tsmMario.Text = "&Mario";
+ tsmMario.Click += Player_Click;
+ //
+ // tsmLuigi
+ //
+ tsmLuigi.Name = "tsmLuigi";
+ tsmLuigi.Size = new Size(131, 26);
+ tsmLuigi.Text = "&Luigi";
+ tsmLuigi.Click += Player_Click;
+ //
+ // tsmViewObjectList
+ //
+ tsmViewObjectList.CheckOnClick = true;
+ tsmViewObjectList.Enabled = false;
+ tsmViewObjectList.Name = "tsmViewObjectList";
+ tsmViewObjectList.Size = new Size(318, 26);
+ tsmViewObjectList.Text = "&Object List (Not yet implemented)";
+ tsmViewObjectList.CheckedChanged += ViewObjectList_CheckedChanged;
+ //
+ // tsmHelp
+ //
+ tsmHelp.DropDownItems.AddRange(new ToolStripItem[] { tsmAutoSave, toolStripSeparator12, tsmSpecialThanks, tsmAbout });
+ tsmHelp.Name = "tsmHelp";
+ tsmHelp.Size = new Size(55, 24);
+ tsmHelp.Text = "&Help";
+ //
+ // tsmAutoSave
+ //
+ tsmAutoSave.Name = "tsmAutoSave";
+ tsmAutoSave.Size = new Size(189, 26);
+ tsmAutoSave.Text = "Auto Save...";
+ tsmAutoSave.Click += AutoSave_Click;
+ //
+ // toolStripSeparator12
+ //
+ toolStripSeparator12.Name = "toolStripSeparator12";
+ toolStripSeparator12.Size = new Size(186, 6);
+ //
+ // tsmSpecialThanks
+ //
+ tsmSpecialThanks.Image = Properties.Resources.hands_clapping_solid;
+ tsmSpecialThanks.Name = "tsmSpecialThanks";
+ tsmSpecialThanks.Size = new Size(189, 26);
+ tsmSpecialThanks.Text = "Special &Thanks";
+ tsmSpecialThanks.Click += SpecialThanks_Click;
+ //
+ // tsmAbout
+ //
+ tsmAbout.Enabled = false;
+ tsmAbout.Image = Properties.Resources.circle_question_regular;
+ tsmAbout.Name = "tsmAbout";
+ tsmAbout.ShortcutKeys = Keys.F1;
+ tsmAbout.Size = new Size(189, 26);
+ tsmAbout.Text = "&About";
+ //
+ // toolStrip
+ //
+ toolStrip.ImageScalingSize = new Size(20, 20);
+ toolStrip.Items.AddRange(new ToolStripItem[] { tsbOpen, tsbSave, toolStripSeparator, tslJumpToArea, ttbJumpToArea, tsbJumpToArea, toolStripSeparator4, tsbLoadAreaByLevel, toolStripSeparator3, tsbUndo, tsbRedo, toolStripSeparator2, tsbCut, tsbCopy, tsbPaste, toolStripSeparator5, tsbAddItem, tsbRemoveItem, tsbDeleteAll, toolStripSeparator6, tsbSpriteMode, toolStripSeparator9, tsbSpecialThanks, tsbHelp });
+ toolStrip.Location = new Point(0, 30);
+ toolStrip.Name = "toolStrip";
+ toolStrip.Size = new Size(1030, 27);
+ toolStrip.TabIndex = 0;
+ toolStrip.Text = "toolStrip1";
+ //
+ // tsbOpen
+ //
+ tsbOpen.DisplayStyle = ToolStripItemDisplayStyle.Image;
+ tsbOpen.Image = Properties.Resources.folder_open_solid;
+ tsbOpen.ImageTransparentColor = Color.Magenta;
+ tsbOpen.Name = "tsbOpen";
+ tsbOpen.Size = new Size(29, 24);
+ tsbOpen.Text = "Open";
+ tsbOpen.Click += Open_Click;
+ //
+ // tsbSave
+ //
+ tsbSave.DisplayStyle = ToolStripItemDisplayStyle.Image;
+ tsbSave.Enabled = false;
+ tsbSave.Image = Properties.Resources.floppy_disk_regular;
+ tsbSave.ImageTransparentColor = Color.Magenta;
+ tsbSave.Name = "tsbSave";
+ tsbSave.Size = new Size(29, 24);
+ tsbSave.Text = "Save";
+ tsbSave.Click += Save_Click;
+ //
+ // toolStripSeparator
+ //
+ toolStripSeparator.Name = "toolStripSeparator";
+ toolStripSeparator.Size = new Size(6, 27);
+ //
+ // tslJumpToArea
+ //
+ tslJumpToArea.Name = "tslJumpToArea";
+ tslJumpToArea.Size = new Size(117, 24);
+ tslJumpToArea.Text = "Jump to area: 0x";
+ //
+ // ttbJumpToArea
+ //
+ ttbJumpToArea.CharacterCasing = CharacterCasing.Upper;
+ ttbJumpToArea.Enabled = false;
+ ttbJumpToArea.MaxLength = 2;
+ ttbJumpToArea.Name = "ttbJumpToArea";
+ ttbJumpToArea.Size = new Size(132, 27);
+ ttbJumpToArea.Text = "0";
+ ttbJumpToArea.KeyDown += JumpToArea_KeyDown;
+ ttbJumpToArea.TextChanged += JumpToArea_TextChanged;
+ //
+ // tsbJumpToArea
+ //
+ tsbJumpToArea.DisplayStyle = ToolStripItemDisplayStyle.Image;
+ tsbJumpToArea.Enabled = false;
+ tsbJumpToArea.Image = Properties.Resources.map_regular;
+ tsbJumpToArea.ImageTransparentColor = Color.Magenta;
+ tsbJumpToArea.Name = "tsbJumpToArea";
+ tsbJumpToArea.Size = new Size(29, 24);
+ tsbJumpToArea.Text = "Jump to area";
+ tsbJumpToArea.Click += JumpToArea_Click;
+ //
+ // toolStripSeparator4
+ //
+ toolStripSeparator4.Name = "toolStripSeparator4";
+ toolStripSeparator4.Size = new Size(6, 27);
+ //
+ // tsbLoadAreaByLevel
+ //
+ tsbLoadAreaByLevel.DisplayStyle = ToolStripItemDisplayStyle.Image;
+ tsbLoadAreaByLevel.Enabled = false;
+ tsbLoadAreaByLevel.Image = Properties.Resources.folder_tree_solid;
+ tsbLoadAreaByLevel.ImageTransparentColor = Color.Magenta;
+ tsbLoadAreaByLevel.Name = "tsbLoadAreaByLevel";
+ tsbLoadAreaByLevel.Size = new Size(29, 24);
+ tsbLoadAreaByLevel.Text = "Load area by level";
+ tsbLoadAreaByLevel.ToolTipText = "Load area by level (not yet implemented)";
+ tsbLoadAreaByLevel.Click += LoadArea_Click;
+ //
+ // toolStripSeparator3
+ //
+ toolStripSeparator3.Name = "toolStripSeparator3";
+ toolStripSeparator3.Size = new Size(6, 27);
+ //
+ // tsbUndo
+ //
+ tsbUndo.DisplayStyle = ToolStripItemDisplayStyle.Image;
+ tsbUndo.Enabled = false;
+ tsbUndo.Image = Properties.Resources.rotate_left_solid;
+ tsbUndo.ImageTransparentColor = Color.Magenta;
+ tsbUndo.Name = "tsbUndo";
+ tsbUndo.Size = new Size(29, 24);
+ tsbUndo.Text = "Undo";
+ tsbUndo.Click += Undo_Click;
+ //
+ // tsbRedo
+ //
+ tsbRedo.DisplayStyle = ToolStripItemDisplayStyle.Image;
+ tsbRedo.Enabled = false;
+ tsbRedo.Image = Properties.Resources.rotate_right_solid;
+ tsbRedo.ImageTransparentColor = Color.Magenta;
+ tsbRedo.Name = "tsbRedo";
+ tsbRedo.Size = new Size(29, 24);
+ tsbRedo.Text = "Redo";
+ tsbRedo.Click += Redo_Click;
+ //
+ // toolStripSeparator2
+ //
+ toolStripSeparator2.Name = "toolStripSeparator2";
+ toolStripSeparator2.Size = new Size(6, 27);
+ //
+ // tsbCut
+ //
+ tsbCut.DisplayStyle = ToolStripItemDisplayStyle.Image;
+ tsbCut.Enabled = false;
+ tsbCut.Image = Properties.Resources.scissors_solid;
+ tsbCut.ImageTransparentColor = Color.Magenta;
+ tsbCut.Name = "tsbCut";
+ tsbCut.Size = new Size(29, 24);
+ tsbCut.Text = "Cut";
+ tsbCut.ToolTipText = "Cut";
+ tsbCut.Click += Cut_Click;
+ //
+ // tsbCopy
+ //
+ tsbCopy.DisplayStyle = ToolStripItemDisplayStyle.Image;
+ tsbCopy.Enabled = false;
+ tsbCopy.Image = Properties.Resources.copy_solid;
+ tsbCopy.ImageTransparentColor = Color.Magenta;
+ tsbCopy.Name = "tsbCopy";
+ tsbCopy.Size = new Size(29, 24);
+ tsbCopy.Text = "Copy";
+ tsbCopy.Click += Copy_Click;
+ //
+ // tsbPaste
+ //
+ tsbPaste.DisplayStyle = ToolStripItemDisplayStyle.Image;
+ tsbPaste.Enabled = false;
+ tsbPaste.Image = Properties.Resources.paste_solid;
+ tsbPaste.ImageTransparentColor = Color.Magenta;
+ tsbPaste.Name = "tsbPaste";
+ tsbPaste.Size = new Size(29, 24);
+ tsbPaste.Text = "Paste";
+ tsbPaste.Click += Paste_Click;
+ //
+ // toolStripSeparator5
+ //
+ toolStripSeparator5.Name = "toolStripSeparator5";
+ toolStripSeparator5.Size = new Size(6, 27);
+ //
+ // tsbAddItem
+ //
+ tsbAddItem.DisplayStyle = ToolStripItemDisplayStyle.Image;
+ tsbAddItem.Enabled = false;
+ tsbAddItem.Image = Properties.Resources.plus_solid;
+ tsbAddItem.ImageTransparentColor = Color.Magenta;
+ tsbAddItem.Name = "tsbAddItem";
+ tsbAddItem.Size = new Size(29, 24);
+ tsbAddItem.Text = "Add Item";
+ tsbAddItem.Click += AddItem_Click;
+ //
+ // tsbRemoveItem
+ //
+ tsbRemoveItem.DisplayStyle = ToolStripItemDisplayStyle.Image;
+ tsbRemoveItem.Enabled = false;
+ tsbRemoveItem.Image = Properties.Resources.minus_solid;
+ tsbRemoveItem.ImageTransparentColor = Color.Magenta;
+ tsbRemoveItem.Name = "tsbRemoveItem";
+ tsbRemoveItem.Size = new Size(29, 24);
+ tsbRemoveItem.Text = "Remove Item";
+ tsbRemoveItem.ToolTipText = "Remove Item";
+ tsbRemoveItem.Click += RemoveItem_Click;
+ //
+ // tsbDeleteAll
+ //
+ tsbDeleteAll.DisplayStyle = ToolStripItemDisplayStyle.Image;
+ tsbDeleteAll.Enabled = false;
+ tsbDeleteAll.Image = Properties.Resources.trash_solid;
+ tsbDeleteAll.ImageTransparentColor = Color.Magenta;
+ tsbDeleteAll.Name = "tsbDeleteAll";
+ tsbDeleteAll.Size = new Size(29, 24);
+ tsbDeleteAll.Text = "Delete All Items";
+ tsbDeleteAll.Click += DeleteAll_Click;
+ //
+ // toolStripSeparator6
+ //
+ toolStripSeparator6.Name = "toolStripSeparator6";
+ toolStripSeparator6.Size = new Size(6, 27);
+ //
+ // tsbSpriteMode
+ //
+ tsbSpriteMode.DisplayStyle = ToolStripItemDisplayStyle.Image;
+ tsbSpriteMode.Image = Properties.Resources.alien;
+ tsbSpriteMode.ImageTransparentColor = Color.Magenta;
+ tsbSpriteMode.Name = "tsbSpriteMode";
+ tsbSpriteMode.Size = new Size(29, 24);
+ tsbSpriteMode.Text = "Sprite Mode";
+ tsbSpriteMode.Click += SpriteMode_Click;
+ //
+ // toolStripSeparator9
+ //
+ toolStripSeparator9.Name = "toolStripSeparator9";
+ toolStripSeparator9.Size = new Size(6, 27);
+ //
+ // tsbSpecialThanks
+ //
+ tsbSpecialThanks.DisplayStyle = ToolStripItemDisplayStyle.Image;
+ tsbSpecialThanks.Image = Properties.Resources.hands_clapping_solid;
+ tsbSpecialThanks.ImageTransparentColor = Color.Magenta;
+ tsbSpecialThanks.Name = "tsbSpecialThanks";
+ tsbSpecialThanks.Size = new Size(29, 24);
+ tsbSpecialThanks.Text = "Special Thanks";
+ tsbSpecialThanks.Click += SpecialThanks_Click;
+ //
+ // tsbHelp
+ //
+ tsbHelp.DisplayStyle = ToolStripItemDisplayStyle.Image;
+ tsbHelp.Enabled = false;
+ tsbHelp.Image = Properties.Resources.circle_question_regular;
+ tsbHelp.ImageTransparentColor = Color.Magenta;
+ tsbHelp.Name = "tsbHelp";
+ tsbHelp.Size = new Size(29, 24);
+ tsbHelp.Text = "Help";
+ //
+ // hsbStartX
+ //
+ hsbStartX.Anchor = AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right;
+ hsbStartX.Enabled = false;
+ hsbStartX.LargeChange = 16;
+ hsbStartX.Location = new Point(0, 433);
+ hsbStartX.Maximum = 16;
+ hsbStartX.Name = "hsbStartX";
+ hsbStartX.Size = new Size(1030, 17);
+ hsbStartX.TabIndex = 4;
+ hsbStartX.ValueChanged += StartX_ValueChanged;
+ //
+ // cmsMain
+ //
+ cmsMain.Enabled = false;
+ cmsMain.ImageScalingSize = new Size(20, 20);
+ cmsMain.Items.AddRange(new ToolStripItem[] { cmiCut, cmiCopy, cmiPaste, toolStripSeparator11, cmiAddItem, cmiRemoveItem, cmiDeleteAll });
+ cmsMain.Name = "cmsMain";
+ cmsMain.Size = new Size(250, 154);
+ //
+ // cmiCut
+ //
+ cmiCut.Enabled = false;
+ cmiCut.Name = "cmiCut";
+ cmiCut.ShortcutKeys = Keys.Control | Keys.X;
+ cmiCut.Size = new Size(249, 24);
+ cmiCut.Text = "C&ut";
+ cmiCut.Click += Cut_Click;
+ //
+ // cmiCopy
+ //
+ cmiCopy.Enabled = false;
+ cmiCopy.Name = "cmiCopy";
+ cmiCopy.ShortcutKeys = Keys.Control | Keys.C;
+ cmiCopy.Size = new Size(249, 24);
+ cmiCopy.Text = "&Copy";
+ cmiCopy.Click += Copy_Click;
+ //
+ // cmiPaste
+ //
+ cmiPaste.Enabled = false;
+ cmiPaste.Name = "cmiPaste";
+ cmiPaste.ShortcutKeys = Keys.Control | Keys.V;
+ cmiPaste.Size = new Size(249, 24);
+ cmiPaste.Text = "&Paste";
+ cmiPaste.Click += Paste_Click;
+ //
+ // toolStripSeparator11
+ //
+ toolStripSeparator11.Name = "toolStripSeparator11";
+ toolStripSeparator11.Size = new Size(246, 6);
+ //
+ // cmiAddItem
+ //
+ cmiAddItem.Enabled = false;
+ cmiAddItem.Name = "cmiAddItem";
+ cmiAddItem.ShortcutKeys = Keys.Insert;
+ cmiAddItem.Size = new Size(249, 24);
+ cmiAddItem.Text = "&Add Item";
+ cmiAddItem.Click += AddItem_Click;
+ //
+ // cmiRemoveItem
+ //
+ cmiRemoveItem.Enabled = false;
+ cmiRemoveItem.Name = "cmiRemoveItem";
+ cmiRemoveItem.ShortcutKeys = Keys.Delete;
+ cmiRemoveItem.Size = new Size(249, 24);
+ cmiRemoveItem.Text = "&Remove Item";
+ cmiRemoveItem.Click += RemoveItem_Click;
+ //
+ // cmiDeleteAll
+ //
+ cmiDeleteAll.Enabled = false;
+ cmiDeleteAll.Name = "cmiDeleteAll";
+ cmiDeleteAll.ShortcutKeys = Keys.Control | Keys.Shift | Keys.Delete;
+ cmiDeleteAll.Size = new Size(249, 24);
+ cmiDeleteAll.Text = "&Delete All";
+ cmiDeleteAll.Click += DeleteAll_Click;
+ //
+ // areaControl
+ //
+ areaControl.Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right;
+ areaControl.BorderStyle = BorderStyle.FixedSingle;
+ areaControl.Enabled = false;
+ areaControl.Location = new Point(0, 69);
+ areaControl.Margin = new Padding(3, 4, 3, 4);
+ areaControl.Name = "areaControl";
+ areaControl.Size = new Size(1029, 365);
+ areaControl.TabIndex = 5;
+ areaControl.Paint += AreaControl_Paint;
+ areaControl.MouseClick += AreaControl_MouseClick;
+ areaControl.MouseDoubleClick += AreaControl_MouseDoubleClick;
+ areaControl.MouseDown += AreaControl_MouseDown;
+ areaControl.MouseMove += AreaControl_MouseMove;
+ areaControl.MouseUp += AreaControl_MouseUp;
+ //
+ // headerEditor
+ //
+ headerEditor.Owner = this;
+ //
+ // objectEditor
+ //
+ objectEditor.Owner = this;
+ //
+ // spriteEditor
+ //
+ spriteEditor.Owner = this;
+ //
+ // saveOnClosePrompt
+ //
+ saveOnClosePrompt.Caption = "Brutario";
+ saveOnClosePrompt.Owner = this;
+ saveOnClosePrompt.Text = "There are unsaved changes. Do you want save them before closing?";
+ //
+ // exceptionHelper
+ //
+ exceptionHelper.Owner = this;
+ exceptionHelper.Title = "Brutario";
+ //
+ // saveFileNameSelector
+ //
+ saveFileNameSelector.FileName = "";
+ saveFileNameSelector.Owner = this;
+ //
+ // openFileNameSelector
+ //
+ openFileNameSelector.FileName = "";
+ openFileNameSelector.Owner = this;
+ //
+ // animationTimer
+ //
+ animationTimer.Interval = 3;
+ animationTimer.Tick += Timer_Elapsed;
+ //
+ // objectListView
+ //
+ objectListView.VisibleChanged += ObjectListView_VisibleChanged;
+ //
+ // autoSaveTimer
+ //
+ autoSaveTimer.Interval = 1000;
+ autoSaveTimer.Tick += AutoSaveTimer_Tick;
+ //
+ // MainForm
+ //
+ AutoScaleDimensions = new SizeF(8F, 20F);
+ AutoScaleMode = AutoScaleMode.Font;
+ ClientSize = new Size(1030, 455);
+ Controls.Add(areaControl);
+ Controls.Add(hsbStartX);
+ Controls.Add(toolStrip);
+ Controls.Add(mnuMain);
+ Icon = (Icon)resources.GetObject("$this.Icon");
+ MainMenuStrip = mnuMain;
+ Margin = new Padding(5, 4, 5, 4);
+ MinimumSize = new Size(1045, 323);
+ Name = "MainForm";
+ StartPosition = FormStartPosition.CenterScreen;
+ Text = "Brutario";
+ WindowState = FormWindowState.Maximized;
+ FormClosing += MainForm_FormClosing;
+ Load += MainForm_Load;
+ mnuMain.ResumeLayout(false);
+ mnuMain.PerformLayout();
+ toolStrip.ResumeLayout(false);
+ toolStrip.PerformLayout();
+ cmsMain.ResumeLayout(false);
+ ResumeLayout(false);
+ PerformLayout();
+ }
+
+ #endregion
+
+ private System.Windows.Forms.MenuStrip mnuMain;
+ private System.Windows.Forms.ToolStripMenuItem tsmFile;
+ private System.Windows.Forms.ToolStripMenuItem tsmOpen;
+ private System.Windows.Forms.ToolStripSeparator toolStripSeparator1;
+ private System.Windows.Forms.ToolStripMenuItem tsmExit;
+ private System.Windows.Forms.ToolStrip toolStrip;
+ private System.Windows.Forms.ToolStripButton tsbOpen;
+ private System.Windows.Forms.ToolStripButton tsbSave;
+ private System.Windows.Forms.ToolStripSeparator toolStripSeparator;
+ private System.Windows.Forms.ToolStripButton tsbCut;
+ private System.Windows.Forms.ToolStripButton tsbCopy;
+ private System.Windows.Forms.ToolStripButton tsbPaste;
+ private System.Windows.Forms.ToolStripSeparator toolStripSeparator2;
+ private System.Windows.Forms.ToolStripButton tsbHelp;
+ private System.Windows.Forms.ToolStripLabel tslJumpToArea;
+ private System.Windows.Forms.ToolStripTextBox ttbJumpToArea;
+ private System.Windows.Forms.ToolStripButton tsbJumpToArea;
+ private System.Windows.Forms.ToolStripSeparator toolStripSeparator3;
+ private System.Windows.Forms.HScrollBar hsbStartX;
+ private System.Windows.Forms.ToolStripMenuItem tsmLevel;
+ private System.Windows.Forms.ToolStripMenuItem tsmLoadArea;
+ private System.Windows.Forms.ToolStripButton tsbLoadAreaByLevel;
+ private System.Windows.Forms.ToolStripSeparator toolStripSeparator4;
+ private System.Windows.Forms.ToolStripMenuItem tsmExportTileData;
+ private System.Windows.Forms.ToolStripMenuItem tsmView;
+ private System.Windows.Forms.ToolStripMenuItem tsmEditHeader;
+ private System.Windows.Forms.ToolStripMenuItem tsmSpriteMode;
+ private System.Windows.Forms.ToolStripMenuItem tsmPlayer;
+ private System.Windows.Forms.ToolStripMenuItem tsmPlayerState;
+ private System.Windows.Forms.ToolStripMenuItem tsmSave;
+ private System.Windows.Forms.ToolStripMenuItem tsmSaveAs;
+ private System.Windows.Forms.ToolStripButton tsbUndo;
+ private System.Windows.Forms.ToolStripButton tsbRedo;
+ private System.Windows.Forms.ToolStripButton tsbSpecialThanks;
+ private System.Windows.Forms.ToolStripButton tsbSpriteMode;
+ private System.Windows.Forms.ToolStripSeparator toolStripSeparator5;
+ private System.Windows.Forms.ToolStripSeparator toolStripSeparator6;
+ private System.Windows.Forms.ToolStripMenuItem tsmEdit;
+ private System.Windows.Forms.ToolStripMenuItem tsmUndo;
+ private System.Windows.Forms.ToolStripMenuItem tsmRedo;
+ private System.Windows.Forms.ToolStripSeparator toolStripSeparator7;
+ private System.Windows.Forms.ToolStripMenuItem tsmCut;
+ private System.Windows.Forms.ToolStripMenuItem tsmCopy;
+ private System.Windows.Forms.ToolStripMenuItem tsmPaste;
+ private System.Windows.Forms.ToolStripMenuItem tsmHelp;
+ private System.Windows.Forms.ToolStripMenuItem tsmSpecialThanks;
+ private System.Windows.Forms.ToolStripMenuItem tsmAbout;
+ private System.Windows.Forms.ToolStripSeparator toolStripSeparator8;
+ private System.Windows.Forms.ToolStripMenuItem tsmAddItem;
+ private System.Windows.Forms.ToolStripMenuItem tsmRemoveItem;
+ private System.Windows.Forms.ToolStripMenuItem tsmDeleteAll;
+ private System.Windows.Forms.ToolStripButton tsbAddItem;
+ private System.Windows.Forms.ToolStripButton tsbRemoveItem;
+ private System.Windows.Forms.ToolStripButton tsbDeleteAll;
+ private System.Windows.Forms.ToolStripSeparator toolStripSeparator9;
+ private System.Windows.Forms.ToolStripMenuItem tsmClose;
+ private System.Windows.Forms.ToolStripSeparator toolStripSeparator10;
+ private System.Windows.Forms.ToolStripMenuItem tsmMario;
+ private System.Windows.Forms.ToolStripMenuItem tsmLuigi;
+ private System.Windows.Forms.ToolStripMenuItem tsmSmall;
+ private System.Windows.Forms.ToolStripMenuItem tsmBig;
+ private System.Windows.Forms.ToolStripMenuItem tsmFire;
+ private System.Windows.Forms.ContextMenuStrip cmsMain;
+ private System.Windows.Forms.ToolStripMenuItem cmiCut;
+ private System.Windows.Forms.ToolStripMenuItem cmiCopy;
+ private System.Windows.Forms.ToolStripMenuItem cmiPaste;
+ private System.Windows.Forms.ToolStripSeparator toolStripSeparator11;
+ private System.Windows.Forms.ToolStripMenuItem cmiAddItem;
+ private System.Windows.Forms.ToolStripMenuItem cmiRemoveItem;
+ private System.Windows.Forms.ToolStripMenuItem cmiDeleteAll;
+ private Controls.DesignControl areaControl;
+ private Views.HeaderEditor headerEditor;
+ private Views.ObjectEditor objectEditor;
+ private Views.SpriteEditor spriteEditor;
+ private Views.SaveOnClosePrompt saveOnClosePrompt;
+ private Views.ExceptionView exceptionHelper;
+ private Views.SaveFileNameSelector saveFileNameSelector;
+ private Views.OpenFileNameSelector openFileNameSelector;
+ private System.Windows.Forms.Timer animationTimer;
+ private ToolStripMenuItem tsmViewObjectList;
+ private Views.ObjectListView objectListView;
+ private System.Windows.Forms.Timer autoSaveTimer;
+ private ToolStripMenuItem tsmAutoSave;
+ private ToolStripSeparator toolStripSeparator12;
+ }
+}
+
diff --git a/src/Brutario.Win/MainForm.cs b/src/Brutario.Win/MainForm.cs
new file mode 100644
index 0000000..16e07a2
--- /dev/null
+++ b/src/Brutario.Win/MainForm.cs
@@ -0,0 +1,659 @@
+//
+// Copyright (c) 2022 spel werdz rite. All rights reserved. Licensed under GNU Affero
+// General Public License. See LICENSE in project root for full license
+// information, or visit https://www.gnu.org/licenses/#AGPL
+//
+
+namespace Brutario.Win;
+
+using System;
+using System.Drawing;
+using System.Globalization;
+using System.Windows.Forms;
+
+using Brutario.Win.Properties;
+
+using Core;
+
+using Maseya.Smas.Smb1;
+
+using static System.Windows.Forms.VisualStyles.VisualStyleElement.Window;
+
+public partial class MainForm : Form, IMainView
+{
+ // TODO(nrg): BrutatioEditor needs to be an interface. It's currently not an
+ // interface because it is rapidly changing and I don't want to keep
+ // changing the interface.
+ public MainForm(BrutarioEditor brutarioEditor)
+ {
+ InitializeComponent();
+ InitializeComponent2();
+
+ Presenter = new MainPresenter(
+ brutarioEditor,
+ this,
+ objectListView,
+ exceptionHelper,
+ openFileNameSelector,
+ saveFileNameSelector,
+ saveOnClosePrompt,
+ headerEditor,
+ objectEditor,
+ spriteEditor);
+ }
+
+ public string Title
+ {
+ get
+ {
+ return Text;
+ }
+
+ set
+ {
+ Text = value;
+ }
+ }
+
+ public bool EditorEnabled
+ {
+ get
+ {
+ return tsmSaveAs.Enabled;
+ }
+
+ set
+ {
+ tsmSaveAs.Enabled =
+ tsmClose.Enabled =
+ tsmLoadArea.Enabled =
+ tsbJumpToArea.Enabled =
+ ttbJumpToArea.Enabled = value;
+ //tsbLoadAreaByLevel.Enabled = value;
+ }
+ }
+
+ public bool MapEditorEnabled
+ {
+ get
+ {
+ return areaControl.Enabled;
+ }
+
+ set
+ {
+ areaControl.Enabled =
+ cmsMain.Enabled =
+ hsbStartX.Enabled =
+ cmiAddItem.Enabled =
+ tsmAddItem.Enabled =
+ tsbAddItem.Enabled =
+ tsmExportTileData.Enabled =
+ tsmEditHeader.Enabled = value;
+
+ UpdateScrollBar();
+ }
+ }
+
+ public bool SaveEnabled
+ {
+ get
+ {
+ return tsmSave.Enabled;
+ }
+
+ set
+ {
+ tsmSave.Enabled =
+ tsbSave.Enabled = value;
+ }
+ }
+
+ public bool UndoEnabled
+ {
+ get
+ {
+ return tsmUndo.Enabled;
+ }
+
+ set
+ {
+ tsmUndo.Enabled =
+ tsbUndo.Enabled = value;
+ }
+ }
+
+ public bool RedoEnabled
+ {
+ get
+ {
+ return tsmRedo.Enabled;
+ }
+
+ set
+ {
+ tsmRedo.Enabled =
+ tsbRedo.Enabled = value;
+ }
+ }
+
+ public bool EditItemEnabled
+ {
+ get
+ {
+ return tsmCopy.Enabled;
+ }
+
+ set
+ {
+ cmiCut.Enabled =
+ tsmCut.Enabled =
+ tsbCut.Enabled =
+ cmiCopy.Enabled =
+ tsmCopy.Enabled =
+ tsbCopy.Enabled =
+ cmiRemoveItem.Enabled =
+ tsmRemoveItem.Enabled =
+ tsbRemoveItem.Enabled = value;
+ }
+ }
+
+ public bool PasteEnabled
+ {
+ get
+ {
+ return tsmPaste.Enabled;
+ }
+ set
+ {
+ tsmPaste.Enabled =
+ tsbPaste.Enabled =
+ cmiPaste.Enabled = value;
+ }
+ }
+
+ public bool DeleteAllEnabled
+ {
+ get
+ {
+ return tsmDeleteAll.Enabled;
+ }
+
+ set
+ {
+ cmiDeleteAll.Enabled =
+ tsmDeleteAll.Enabled =
+ tsbDeleteAll.Enabled = value;
+ }
+ }
+
+ public bool SpriteMode
+ {
+ get
+ {
+ return tsmSpriteMode.Checked;
+ }
+
+ set
+ {
+ tsmSpriteMode.Checked =
+ tsbSpriteMode.Checked = value;
+ }
+ }
+
+ public Player Player
+ {
+ get
+ {
+ return tsmMario.Checked ? Player.Mario : Player.Luigi;
+ }
+
+ set
+ {
+ tsmMario.Checked = value == Player.Mario;
+ tsmLuigi.Checked = value == Player.Luigi;
+ }
+ }
+
+ public PlayerState PlayerState
+ {
+ get
+ {
+ return tsmSmall.Checked
+ ? PlayerState.Small
+ : tsmBig.Checked
+ ? PlayerState.Big
+ : PlayerState.Fire;
+ }
+
+ set
+ {
+ tsmSmall.Checked = value == PlayerState.Small;
+ tsmBig.Checked = value == PlayerState.Big;
+ tsmFire.Checked = value == PlayerState.Fire;
+ }
+ }
+
+ public Size DrawAreaSize
+ {
+ get
+ {
+ return areaControl.ClientSize;
+ }
+ }
+
+ public int AreaNumber
+ {
+ get
+ {
+ return Int32.TryParse(
+ ttbJumpToArea.Text,
+ NumberStyles.HexNumber,
+ CultureInfo.CurrentUICulture,
+ out var areaNumber)
+ ? areaNumber
+ : -1;
+ }
+
+ set
+ {
+ ttbJumpToArea.Text = $"{value:X2}";
+ }
+ }
+
+ public bool JumpToAreaEnabled
+ {
+ get
+ {
+ return tsbJumpToArea.Enabled;
+ }
+
+ set
+ {
+ tsbJumpToArea.Enabled = value;
+ }
+ }
+
+ public int StartX
+ {
+ get
+ {
+ return hsbStartX.Value;
+ }
+
+ set
+ {
+ hsbStartX.Value = value;
+ }
+ }
+
+ public MainPresenter Presenter { get; }
+
+ private DateTime StartTime { get; set; }
+
+ private TimeSpan ElapsedTime
+ {
+ get
+ {
+ return DateTime.Now - StartTime;
+ }
+ }
+
+ private int CurrentFrame
+ {
+ get
+ {
+ // TODO(nrg): Remove frame constants here.
+ return (int)(ElapsedTime.TotalMilliseconds * (60 / 1000.0));
+ }
+ }
+
+ public void Redraw()
+ {
+ areaControl.Invalidate();
+ }
+
+ ///
+ /// Initialize remaining components that could not be initialized in
+ /// through the designer.
+ ///
+ private void InitializeComponent2()
+ {
+ tsmMario.Tag = Player.Mario;
+ tsmLuigi.Tag = Player.Luigi;
+
+ tsmSmall.Tag = PlayerState.Small;
+ tsmBig.Tag = PlayerState.Big;
+ tsmFire.Tag = PlayerState.Fire;
+
+ autoSaveTimer.Interval = (int)new TimeSpan(0, 0, 3).TotalMilliseconds;
+ }
+
+ private void MainForm_Load(object sender, EventArgs e)
+ {
+
+ if (Presenter.AutoSaveEnabled = Settings.Default.AutoSaveEnabled)
+ {
+ Presenter.AutoSaveInterval = Settings.Default.AutoSaveInterval;
+ }
+
+ if (Presenter.PruneAutoSavesEnabled = Settings.Default.AutoSavePruningEnabled)
+ {
+ Presenter.AutoSaveCutoffAge = Settings.Default.AutoSavePruningCutoff;
+ }
+
+ Presenter.AutoSaveHardCutoff = Settings.Default.AutoSaveHardCutoff;
+
+ // TODO(swr): Maybe start timer when rom is opened?
+ StartTime = DateTime.Now;
+ animationTimer.Start();
+
+ autoSaveTimer.Start();
+ }
+
+ private void Open_Click(object? sender, EventArgs e)
+ {
+ // TODO(nrg): Should these presenter commands (or some work inside of them) be
+ // asynchronous? Open is a good first consideration because it needs to block
+ // for the open file dialog. In fact, showDialog cannot even be done
+ // asynchronously. So we cannot await here. Awaiting needs to happen inside the
+ // presenter logic. Put another way, the presenter logic must take place on the
+ // UI thread.
+ //
+ // Another caveat is whether the presenter should handle the open file dialog
+ // logic. Open_Click could have an open file dialog and handle all that logic
+ // here. However, other dialogs I've made do not play nicely with this idea
+ // since they change some visuals for preview stuff. But those changes are sent
+ // through events, so maybe it's ok? MVP is painful.
+ //
+ // Let's look for another dialog command and make considerations there.
+ Presenter.Open();
+ }
+
+ private void Save_Click(object? sender, EventArgs e)
+ {
+ Presenter.Save();
+ }
+
+ private void SaveAs_Click(object? sender, EventArgs e)
+ {
+ Presenter.SaveAs();
+ }
+
+ private void Close_Click(object? sender, EventArgs e)
+ {
+ _ = Presenter.Close();
+ }
+
+ private void Exit_Click(object? sender, EventArgs e)
+ {
+ Close();
+ }
+
+ private void Undo_Click(object? sender, EventArgs e)
+ {
+ Presenter.Undo();
+ }
+
+ private void Redo_Click(object? sender, EventArgs e)
+ {
+ Presenter.Redo();
+ }
+
+ private void Cut_Click(object? sender, EventArgs e)
+ {
+ Presenter.CutCurrentItem();
+ }
+
+ private void Copy_Click(object? sender, EventArgs e)
+ {
+ Presenter.CopyCurrentItem();
+ }
+
+ private void Paste_Click(object? sender, EventArgs e)
+ {
+ Presenter.Paste();
+ }
+
+ private void AddItem_Click(object? sender, EventArgs e)
+ {
+ Presenter.AddItem();
+ }
+
+ private void RemoveItem_Click(object? sender, EventArgs e)
+ {
+ Presenter.RemoveCurrentItem();
+ }
+
+ private void DeleteAll_Click(object? sender, EventArgs e)
+ {
+ Presenter.DeleteAllItems();
+ }
+
+ private void ExportTileData_Click(object? sender, EventArgs e)
+ {
+ try
+ {
+ Presenter.ExportTileData();
+ }
+ catch (Exception ex)
+ {
+ _ = MessageBox.Show(ex.Message);
+ }
+ }
+
+ private void EditHeader_Click(object? sender, EventArgs e)
+ {
+ Presenter.EditHeader();
+ }
+
+ private void SpriteMode_Click(object? sender, EventArgs e)
+ {
+ if (sender is ToolStripMenuItem tsm)
+ {
+ // For some reason, menu items get checked before the click event...
+ Presenter.ToggleSpritMode(tsm.Checked);
+ }
+ else if (sender is ToolStripButton tsb)
+ {
+ // but tool strip buttons get checked after. >_<
+ Presenter.ToggleSpritMode(!tsb.Checked);
+ }
+ }
+
+ private void Player_Click(object? sender, EventArgs e)
+ {
+ Presenter.SetPlayer((Player)(sender as ToolStripMenuItem)!.Tag!);
+ }
+
+ private void PlayerState_Click(object? sender, EventArgs e)
+ {
+ Presenter.SetPlayerState(
+ (PlayerState)(sender as ToolStripMenuItem)!.Tag!);
+ }
+
+ private void SpecialThanks_Click(object? sender, EventArgs e)
+ {
+ // TODO(nrg): Here's another example of whether this should be in the control
+ // logic. My guess is no, as that implies attribution should be part of the API.
+ using var dialog = new SpecialThanksForm();
+ _ = dialog.ShowDialog(this);
+ }
+
+ private void MainForm_FormClosing(object? sender, FormClosingEventArgs e)
+ {
+ if (e.CloseReason != CloseReason.TaskManagerClosing && !Presenter.Close())
+ {
+ e.Cancel = true;
+ }
+ }
+
+ private void StartX_ValueChanged(object? sender, EventArgs e)
+ {
+ Presenter.SetStartX(StartX);
+ }
+
+ private void AreaControl_SizeChanged(object? sender, EventArgs e)
+ {
+ UpdateScrollBar();
+ }
+
+ private void AreaControl_Paint(object? sender, PaintEventArgs e)
+ {
+ if (areaControl.Enabled)
+ {
+ var drawData = Presenter.GetDrawData(
+ startX: StartX,
+ size: areaControl.Size,
+ separatorColor: Color.Blue,
+ passiveColor: Color.LightGreen,
+ selectColor: Color.White);
+ AreaPixelRenderer.DrawArea(e.Graphics, drawData);
+ }
+ }
+
+ private void AreaControl_MouseClick(object? sender, MouseEventArgs e)
+ {
+ switch (e.Button)
+ {
+ case MouseButtons.Right:
+ Presenter.SetSelectedItem((e.X + (StartX * 8)) >> 4, e.Y >> 4);
+ if (cmsMain.Enabled)
+ {
+ cmsMain.Show(areaControl, e.Location);
+ }
+
+ break;
+ }
+ }
+
+ private void AreaControl_MouseDown(object? sender, MouseEventArgs e)
+ {
+ switch (e.Button)
+ {
+ case MouseButtons.Left:
+ Presenter.SetSelectedItem((e.X + (StartX * 8)) >> 4, e.Y >> 4);
+ Presenter.InitializeMoveItem();
+ break;
+ }
+ }
+
+ private void AreaControl_MouseMove(object sender, MouseEventArgs e)
+ {
+ if (e.Button == MouseButtons.Left)
+ {
+ Presenter.MoveSelectedItem((e.X + (StartX * 8)) >> 4, e.Y >> 4);
+ }
+ }
+
+ private void AreaControl_MouseUp(object sender, MouseEventArgs e)
+ {
+ if (e.Button == MouseButtons.Left)
+ {
+ Presenter.FinishEditItem(commit: true);
+ }
+ }
+
+ private void AreaControl_MouseDoubleClick(object? sender, MouseEventArgs e)
+ {
+ if (e.Button == MouseButtons.Left)
+ {
+ Presenter.EditSelectedItem();
+ }
+ }
+
+ private void JumpToArea_TextChanged(object? sender, EventArgs e)
+ {
+ JumpToAreaEnabled = Presenter.IsValidAreaNumber(AreaNumber);
+ }
+
+ private void JumpToArea_Click(object? sender, EventArgs e)
+ {
+ if (AreaNumber != -1)
+ {
+ Presenter.SetAreaNumber(AreaNumber);
+ }
+ }
+
+ private void JumpToArea_KeyDown(object? sender, KeyEventArgs e)
+ {
+ if (e.KeyCode == Keys.Enter && tsbJumpToArea.Enabled)
+ {
+ tsbJumpToArea.PerformClick();
+ e.SuppressKeyPress = true;
+ }
+ }
+
+ private void UpdateScrollBar()
+ {
+ // TODO(nrg): There are quite a few assumption here. Let's remove some
+ // of these constants.
+ var viewWidth = ((areaControl.ClientSize.Width - 1) / 8) + 1;
+ hsbStartX.Maximum = 0x400 - viewWidth;
+ hsbStartX.SmallChange = 1;
+ hsbStartX.LargeChange = 0x20;
+ }
+
+ private void Timer_Elapsed(object? sender, EventArgs e)
+ {
+ // TODO(nrg): Should animation be UI-controlled?
+ Presenter.UpdateFrame(CurrentFrame);
+ }
+
+ private void LoadArea_Click(object sender, EventArgs e)
+ {
+ MessageBox.Show("Not yet implemented");
+ }
+
+ private void ObjectListView_AddItem_Click(object sender, EventArgs e)
+ {
+ Presenter.AddItem();
+ }
+
+ private void ViewObjectList_CheckedChanged(object sender, EventArgs e)
+ {
+ objectListView.Visible = tsmViewObjectList.Checked;
+ }
+
+ private void ObjectListView_VisibleChanged(object sender, EventArgs e)
+ {
+ tsmViewObjectList.Checked = objectListView.Visible;
+ }
+
+ private void AutoSaveTimer_Tick(object sender, EventArgs e)
+ {
+ Presenter.AutoSave();
+ }
+
+ private void AutoSave_Click(object sender, EventArgs e)
+ {
+ using var dialog = new AutoSaveForm();
+
+ if (dialog.EnableAutoSave = Presenter.AutoSaveEnabled)
+ {
+ dialog.AutoSaveInterval = Presenter.AutoSaveInterval;
+ }
+
+ if (dialog.EnablePruning = Presenter.PruneAutoSavesEnabled)
+ {
+ dialog.PruningInterval = Presenter.AutoSaveCutoffAge;
+ }
+
+ dialog.HardCutoff = Presenter.AutoSaveHardCutoff;
+ if (dialog.ShowDialog(this) == DialogResult.OK)
+ {
+ if (Settings.Default.AutoSaveEnabled = Presenter.AutoSaveEnabled = dialog.EnableAutoSave)
+ {
+ Settings.Default.AutoSaveInterval = Presenter.AutoSaveInterval = dialog.AutoSaveInterval;
+ }
+
+ if (Settings.Default.AutoSavePruningEnabled = Presenter.PruneAutoSavesEnabled = dialog.EnablePruning)
+ {
+ Settings.Default.AutoSavePruningCutoff = Presenter.AutoSaveCutoffAge = dialog.PruningInterval;
+ }
+
+ Settings.Default.AutoSaveHardCutoff = Presenter.AutoSaveHardCutoff = dialog.HardCutoff;
+ Settings.Default.Save();
+ }
+ }
+}
diff --git a/src/Brutario.Win/MainForm.resx b/src/Brutario.Win/MainForm.resx
new file mode 100644
index 0000000..8f1c674
--- /dev/null
+++ b/src/Brutario.Win/MainForm.resx
@@ -0,0 +1,998 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ 182, 17
+
+
+ 304, 17
+
+
+ 422, 17
+
+
+ 541, 17
+
+
+ 250
+
+
+
+
+ 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
+ +Pj/+Pj4//j4+P/4+Pj/+Pj4/4Cw+P8oKCj/AAAAAAAAAAAAAAAAAAAAAAAAAAAoKCj/KCgo/3jQ+P/4
+ +Pj/+Pj4//j4+P/4+Pj/+Pj4//j4+P940Pj/KCgo/wAAAAAAAAAAAAAAAAAAAAAAAAAAKCgo/4Cw+P/4
+ +Pj/+Pj4//j4+P/4+Pj/+Pj4//j4+P/4+Pj/+Pj4/4Cw+P8oKCj/AAAAAAAAAAAAAAAAAAAAACgoKP+A
+ sPj/+Pj4//j4+P/4+Pj/+Pj4//j4+P/4+Pj/+Pj4//j4+P+AsPj/KCgo/wAAAAAAAAAAAAAAAAAAAAAo
+ KCj/eND4//j4+P/4+Pj/+Pj4//j4+P/4+Pj/+Pj4//j4+P/4+Pj/eND4/ygoKP8AAAAAAAAAAAAAAAAA
+ AAAAKCgo/4Cw+P/4+Pj/+Pj4/ygoKP/4+Pj/+Pj4/ygoKP/4+Pj/+Pj4/4Cw+P8oKCj/AAAAAAAAAAAA
+ AAAAAAAAACgoKP+AsPj/+Pj4//j4+P8oKCj/+Pj4//j4+P8oKCj/+Pj4//j4+P+AsPj/KCgo/wAAAAAA
+ AAAAAAAAAAAAAAAoKCj/eND4//j4+P/4+Pj/KCgo//j4+P/4+Pj/KCgo//j4+P/4+Pj/eND4/ygoKP8A
+ AAAAAAAAAAAAAAAoKCj/KCgo/ygoKP+AsPj/gLD4/ygoKP+AsPj/gLD4/ygoKP+AsPj/gLD4/ygoKP8o
+ KCj/KCgo/wAAAAAAAAAAKCgo/ygoKP8oKCj/gLD4/4Cw+P8oKCj/gLD4/4Cw+P8oKCj/gLD4/4Cw+P8o
+ KCj/KCgo/ygoKP8AAAAAAAAAACgoKP8oKCj/KCgo/3jQ+P940Pj/KCgo/3jQ+P940Pj/KCgo/3jQ+P94
+ 0Pj/KCgo/ygoKP8oKCj/AAAAACgoKP+oAAD/qAAA/6gAAP8oKCj/KCgo/ygoKP8oKCj/KCgo/ygoKP8o
+ KCj/KCgo/6gAAP+oAAD/qAAA/ygoKP8oKCj/iagA/4moAP+JqAD/KCgo/ygoKP8oKCj/KCgo/ygoKP8o
+ KCj/KCgo/ygoKP+JqAD/iagA/4moAP8oKCj/KCgo/wCAGP8AgBj/AIAY/ygoKP8oKCj/KCgo/ygoKP8o
+ KCj/KCgo/ygoKP8oKCj/AIAY/wCAGP8AgBj/KCgo/ygoKP+oAAD/8AcH//AHB/+oAAD/qAAA//j4+P/4
+ +Pj/+Pj4//j4+P+oAAD/qAAA//AHB//wBwf/qAAA/ygoKP8oKCj/iagA/8XwB//F8Af/iagA/4moAP/4
+ +Pj/+Pj4//j4+P/4+Pj/iagA/4moAP/F8Af/xfAH/4moAP8oKCj/KCgo/wCAGP8YwFD/GMBQ/wCAGP8A
+ gBj/+Pj4//j4+P/4+Pj/+Pj4/wCAGP8AgBj/GMBQ/xjAUP8AgBj/KCgo/ygoKP/4+Pj/8AcH//AHB//w
+ Bwf/+Pj4//j4+P/4+Pj/+Pj4//j4+P/4+Pj/8AcH//AHB//wBwf/+Pj4/ygoKP8oKCj/+Pj4/8XwB//F
+ 8Af/xfAH//j4+P/4+Pj/+Pj4//j4+P/4+Pj/+Pj4/8XwB//F8Af/xfAH//j4+P8oKCj/KCgo//j4+P8Y
+ wFD/GMBQ/xjAUP/4+Pj/+Pj4//j4+P/4+Pj/+Pj4//j4+P8YwFD/GMBQ/xjAUP/4+Pj/KCgo/ygoKP/4
+ +Pj/+Pj4//AHB//3YGD/+Pj4//j4+P/4+Pj/+Pj4//j4+P/4+Pj/92Bg//AHB//4+Pj/+Pj4/ygoKP8o
+ KCj/+Pj4//j4+P/F8Af/3Pdg//j4+P/4+Pj/+Pj4//j4+P/4+Pj/+Pj4/9z3YP/F8Af/+Pj4//j4+P8o
+ KCj/KCgo//j4+P/4+Pj/GMBQ/1D4iP/4+Pj/+Pj4//j4+P/4+Pj/+Pj4//j4+P9Q+Ij/GMBQ//j4+P/4
+ +Pj/KCgo/ygoKP/4+Pj/+Pj4//AHB//3YGD/+Pj4//j4+P/4+Pj/+Pj4//j4+P/4+Pj/92Bg//AHB//4
+ +Pj/+Pj4/ygoKP8oKCj/+Pj4//j4+P/F8Af/3Pdg//j4+P/4+Pj/+Pj4//j4+P/4+Pj/+Pj4/9z3YP/F
+ 8Af/+Pj4//j4+P8oKCj/KCgo//j4+P/4+Pj/GMBQ/1D4iP/4+Pj/+Pj4//j4+P/4+Pj/+Pj4//j4+P9Q
+ +Ij/GMBQ//j4+P/4+Pj/KCgo/ygoKP/4+Pj/8AcH//AHB//3YGD/92Bg//j4+P/4+Pj/+Pj4//j4+P/3
+ YGD/92Bg//AHB//wBwf/+Pj4/ygoKP8oKCj/+Pj4/8XwB//F8Af/3Pdg/9z3YP/4+Pj/+Pj4//j4+P/4
+ +Pj/3Pdg/9z3YP/F8Af/xfAH//j4+P8oKCj/KCgo//j4+P8YwFD/GMBQ/1D4iP9Q+Ij/+Pj4//j4+P/4
+ +Pj/+Pj4/1D4iP9Q+Ij/GMBQ/xjAUP/4+Pj/KCgo/wAAAAAoKCj/qAAA//AHB//wBwf/92Bg//dgYP/3
+ YGD/92Bg//dgYP/3YGD/8AcH//AHB/+oAAD/KCgo/wAAAAAAAAAAKCgo/4moAP/F8Af/xfAH/9z3YP/c
+ 92D/3Pdg/9z3YP/c92D/3Pdg/8XwB//F8Af/iagA/ygoKP8AAAAAAAAAACgoKP8AgBj/GMBQ/xjAUP9Q
+ +Ij/UPiI/1D4iP9Q+Ij/UPiI/1D4iP8YwFD/GMBQ/wCAGP8oKCj/AAAAAAAAAAAoKCj/qAAA/6gAAP/w
+ Bwf/8AcH//dgYP/3YGD/92Bg//dgYP/wBwf/8AcH/6gAAP+oAAD/KCgo/wAAAAAAAAAAKCgo/4moAP+J
+ qAD/xfAH/8XwB//c92D/3Pdg/9z3YP/c92D/xfAH/8XwB/+JqAD/iagA/ygoKP8AAAAAAAAAACgoKP8A
+ gBj/AIAY/xjAUP8YwFD/UPiI/1D4iP9Q+Ij/UPiI/xjAUP8YwFD/AIAY/wCAGP8oKCj/AAAAAAAAAAAA
+ AAAAKCgo//j4+P/4+Pj/+Pj4//AHB//wBwf/8AcH//AHB//4+Pj/+Pj4//j4+P8oKCj/AAAAAAAAAAAA
+ AAAAAAAAACgoKP/4+Pj/+Pj4//j4+P/F8Af/xfAH/8XwB//F8Af/+Pj4//j4+P/4+Pj/KCgo/wAAAAAA
+ AAAAAAAAAAAAAAAoKCj/+Pj4//j4+P/4+Pj/GMBQ/xjAUP8YwFD/GMBQ//j4+P/4+Pj/+Pj4/ygoKP8A
+ AAAAAAAAAAAAAAAAAAAAAAAAACgoKP8oKCj/+Pj4//j4+P+oAAD/qAAA//j4+P/4+Pj/KCgo/ygoKP8A
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAoKCj/KCgo//j4+P/4+Pj/iagA/4moAP/4+Pj/+Pj4/ygoKP8o
+ KCj/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKCgo/ygoKP/4+Pj/+Pj4/wCAGP8AgBj/+Pj4//j4+P8o
+ KCj/KCgo/wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKCgo/ygoKP8oKCj/KCgo/ygoKP8o
+ KCj/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACgoKP8oKCj/KCgo/ygoKP8o
+ KCj/KCgo/wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAoKCj/KCgo/ygoKP8o
+ KCj/KCgo/ygoKP8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAoKCj/KCgo/ygoKP8o
+ KCj/KCgo/ygoKP8oKCj/KCgo/wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKCgo/ygoKP8o
+ KCj/KCgo/ygoKP8oKCj/KCgo/ygoKP8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACgoKP8o
+ KCj/KCgo/ygoKP8oKCj/KCgo/ygoKP8oKCj/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKCgo/ygoKP+A
+ sPj/+Pj4//j4+P/4+Pj/+Pj4//j4+P/4+Pj/gLD4/ygoKP8AAAAAAAAAAAAAAAAAAAAAAAAAACgoKP8o
+ KCj/wMDA//j4+P/4+Pj/+Pj4//j4+P/4+Pj/+Pj4/8DAwP8oKCj/AAAAAAAAAAAAAAAAAAAAAAAAAAAo
+ KCj/KCgo/4Cw+P/4+Pj/+Pj4//j4+P/4+Pj/+Pj4//j4+P+AsPj/KCgo/wAAAAAAAAAAAAAAAAAAAAAA
+ AAAAKCgo/4Cw+P/4+Pj/+Pj4//j4+P/4+Pj/+Pj4//j4+P/4+Pj/+Pj4/4Cw+P8oKCj/AAAAAAAAAAAA
+ AAAAAAAAACgoKP/AwMD/+Pj4//j4+P/4+Pj/+Pj4//j4+P/4+Pj/+Pj4//j4+P/AwMD/KCgo/wAAAAAA
+ AAAAAAAAAAAAAAAoKCj/gLD4//j4+P/4+Pj/+Pj4//j4+P/4+Pj/+Pj4//j4+P/4+Pj/gLD4/ygoKP8A
+ AAAAAAAAAAAAAAAAAAAAKCgo/4Cw+P/4+Pj/+Pj4/ygoKP/4+Pj/+Pj4/ygoKP/4+Pj/+Pj4/4Cw+P8o
+ KCj/AAAAAAAAAAAAAAAAAAAAACgoKP/AwMD/+Pj4//j4+P8oKCj/+Pj4//j4+P8oKCj/+Pj4//j4+P/A
+ wMD/KCgo/wAAAAAAAAAAAAAAAAAAAAAoKCj/gLD4//j4+P/4+Pj/KCgo//j4+P/4+Pj/KCgo//j4+P/4
+ +Pj/gLD4/ygoKP8AAAAAAAAAAAAAAAAoKCj/KCgo/ygoKP+AsPj/gLD4/ygoKP+AsPj/gLD4/ygoKP+A
+ sPj/gLD4/ygoKP8oKCj/KCgo/wAAAAAAAAAAKCgo/ygoKP8oKCj/wMDA/8DAwP8oKCj/wMDA/8DAwP8o
+ KCj/wMDA/8DAwP8oKCj/KCgo/ygoKP8AAAAAAAAAACgoKP8oKCj/KCgo/4Cw+P+AsPj/KCgo/4Cw+P+A
+ sPj/KCgo/4Cw+P+AsPj/KCgo/ygoKP8oKCj/AAAAACgoKP+oAFr/qABa/6gAWv8oKCj/KCgo/ygoKP8o
+ KCj/KCgo/ygoKP8oKCj/KCgo/6gAWv+oAFr/qABa/ygoKP8oKCj/Ozs7/zs7O/87Ozv/KCgo/ygoKP8o
+ KCj/KCgo/ygoKP8oKCj/KCgo/ygoKP87Ozv/Ozs7/zs7O/8oKCj/KCgo/wCojP8AqIz/AKiM/ygoKP8o
+ KCj/KCgo/ygoKP8oKCj/KCgo/ygoKP8oKCj/AKiM/wCojP8AqIz/KCgo/ygoKP+oAFr/8AeD//AHg/+o
+ AFr/qABa//j4+P/4+Pj/+Pj4//j4+P+oAFr/qABa//AHg//wB4P/qABa/ygoKP8oKCj/Ozs7/09PT/9P
+ T0//Ozs7/zs7O//4+Pj/+Pj4//j4+P/4+Pj/Ozs7/zs7O/9PT0//T09P/zs7O/8oKCj/KCgo/wCojP8H
+ 8Mn/B/DJ/wCojP8AqIz/+Pj4//j4+P/4+Pj/+Pj4/wCojP8AqIz/B/DJ/wfwyf8AqIz/KCgo/ygoKP/4
+ +Pj/8AeD//AHg//wB4P/+Pj4//j4+P/4+Pj/+Pj4//j4+P/4+Pj/8AeD//AHg//wB4P/+Pj4/ygoKP8o
+ KCj/+Pj4/09PT/9PT0//T09P//j4+P/4+Pj/+Pj4//j4+P/4+Pj/+Pj4/09PT/9PT0//T09P//j4+P8o
+ KCj/KCgo//j4+P8H8Mn/B/DJ/wfwyf/4+Pj/+Pj4//j4+P/4+Pj/+Pj4//j4+P8H8Mn/B/DJ/wfwyf/4
+ +Pj/KCgo/ygoKP/4+Pj/+Pj4//AHg//3YLH/+Pj4//j4+P/4+Pj/+Pj4//j4+P/4+Pj/92Cx//AHg//4
+ +Pj/+Pj4/ygoKP8oKCj/+Pj4//j4+P9PT0//kJCQ//j4+P/4+Pj/+Pj4//j4+P/4+Pj/+Pj4/5CQkP9P
+ T0//+Pj4//j4+P8oKCj/KCgo//j4+P/4+Pj/B/DJ/2D33v/4+Pj/+Pj4//j4+P/4+Pj/+Pj4//j4+P9g
+ 997/B/DJ//j4+P/4+Pj/KCgo/ygoKP/4+Pj/+Pj4//AHg//3YLH/+Pj4//j4+P/4+Pj/+Pj4//j4+P/4
+ +Pj/92Cx//AHg//4+Pj/+Pj4/ygoKP8oKCj/+Pj4//j4+P9PT0//kJCQ//j4+P/4+Pj/+Pj4//j4+P/4
+ +Pj/+Pj4/5CQkP9PT0//+Pj4//j4+P8oKCj/KCgo//j4+P/4+Pj/B/DJ/2D33v/4+Pj/+Pj4//j4+P/4
+ +Pj/+Pj4//j4+P9g997/B/DJ//j4+P/4+Pj/KCgo/ygoKP/4+Pj/8AeD//AHg//3YLH/92Cx//j4+P/4
+ +Pj/+Pj4//j4+P/3YLH/92Cx//AHg//wB4P/+Pj4/ygoKP8oKCj/+Pj4/09PT/9PT0//kJCQ/5CQkP/4
+ +Pj/+Pj4//j4+P/4+Pj/kJCQ/5CQkP9PT0//T09P//j4+P8oKCj/KCgo//j4+P8H8Mn/B/DJ/2D33v9g
+ 997/+Pj4//j4+P/4+Pj/+Pj4/2D33v9g997/B/DJ/wfwyf/4+Pj/KCgo/wAAAAAoKCj/qABa//AHg//w
+ B4P/92Cx//dgsf/3YLH/92Cx//dgsf/3YLH/8AeD//AHg/+oAFr/KCgo/wAAAAAAAAAAKCgo/zs7O/9P
+ T0//T09P/5CQkP+QkJD/kJCQ/5CQkP+QkJD/kJCQ/09PT/9PT0//Ozs7/ygoKP8AAAAAAAAAACgoKP8A
+ qIz/B/DJ/wfwyf9g997/YPfe/2D33v9g997/YPfe/2D33v8H8Mn/B/DJ/wCojP8oKCj/AAAAAAAAAAAo
+ KCj/qABa/6gAWv/wB4P/8AeD//dgsf/3YLH/92Cx//dgsf/wB4P/8AeD/6gAWv+oAFr/KCgo/wAAAAAA
+ AAAAKCgo/zs7O/87Ozv/T09P/09PT/+QkJD/kJCQ/5CQkP+QkJD/T09P/09PT/87Ozv/Ozs7/ygoKP8A
+ AAAAAAAAACgoKP8AqIz/AKiM/wfwyf8H8Mn/YPfe/2D33v9g997/YPfe/wfwyf8H8Mn/AKiM/wCojP8o
+ KCj/AAAAAAAAAAAAAAAAKCgo//j4+P/4+Pj/+Pj4//AHg//wB4P/8AeD//AHg//4+Pj/+Pj4//j4+P8o
+ KCj/AAAAAAAAAAAAAAAAAAAAACgoKP/4+Pj/+Pj4//j4+P9PT0//T09P/09PT/9PT0//+Pj4//j4+P/4
+ +Pj/KCgo/wAAAAAAAAAAAAAAAAAAAAAoKCj/+Pj4//j4+P/4+Pj/B/DJ/wfwyf8H8Mn/B/DJ//j4+P/4
+ +Pj/+Pj4/ygoKP8AAAAAAAAAAAAAAAAAAAAAAAAAACgoKP8oKCj/+Pj4//j4+P+oAFr/qABa//j4+P/4
+ +Pj/KCgo/ygoKP8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAoKCj/KCgo//j4+P/4+Pj/Ozs7/zs7O//4
+ +Pj/+Pj4/ygoKP8oKCj/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKCgo/ygoKP/4+Pj/+Pj4/wCojP8A
+ qIz/+Pj4//j4+P8oKCj/KCgo/wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKCgo/ygoKP8o
+ KCj/KCgo/ygoKP8oKCj/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACgoKP8o
+ KCj/KCgo/ygoKP8oKCj/KCgo/wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAo
+ KCj/KCgo/ygoKP8oKCj/KCgo/ygoKP8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAo
+ KCj/KCgo/ygoKP8oKCj/KCgo/ygoKP8oKCj/KCgo/wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAKCgo/ygoKP8oKCj/KCgo/ygoKP8oKCj/KCgo/ygoKP8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAACgoKP8oKCj/KCgo/ygoKP8oKCj/KCgo/ygoKP8oKCj/AAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAKCgo/ygoKP+AsPj/+Pj4//j4+P/4+Pj/+Pj4//j4+P/4+Pj/gLD4/ygoKP8AAAAAAAAAAAAAAAAA
+ AAAAAAAAACgoKP8oKCj/gLD4//j4+P/4+Pj/+Pj4//j4+P/4+Pj/+Pj4/4Cw+P8oKCj/AAAAAAAAAAAA
+ AAAAAAAAAAAAAAAoKCj/KCgo/4Cw+P/4+Pj/+Pj4//j4+P/4+Pj/+Pj4//j4+P+AsPj/KCgo/wAAAAAA
+ AAAAAAAAAAAAAAAAAAAAKCgo/4Cw+P/4+Pj/+Pj4//j4+P/4+Pj/+Pj4//j4+P/4+Pj/+Pj4/4Cw+P8o
+ KCj/AAAAAAAAAAAAAAAAAAAAACgoKP+AsPj/+Pj4//j4+P/4+Pj/+Pj4//j4+P/4+Pj/+Pj4//j4+P+A
+ sPj/KCgo/wAAAAAAAAAAAAAAAAAAAAAoKCj/gLD4//j4+P/4+Pj/+Pj4//j4+P/4+Pj/+Pj4//j4+P/4
+ +Pj/gLD4/ygoKP8AAAAAAAAAAAAAAAAAAAAAKCgo/4Cw+P/4+Pj/+Pj4/ygoKP/4+Pj/+Pj4/ygoKP/4
+ +Pj/+Pj4/4Cw+P8oKCj/AAAAAAAAAAAAAAAAAAAAACgoKP+AsPj/+Pj4//j4+P8oKCj/+Pj4//j4+P8o
+ KCj/+Pj4//j4+P+AsPj/KCgo/wAAAAAAAAAAAAAAAAAAAAAoKCj/gLD4//j4+P/4+Pj/KCgo//j4+P/4
+ +Pj/KCgo//j4+P/4+Pj/gLD4/ygoKP8AAAAAAAAAAAAAAAAoKCj/KCgo/ygoKP+AsPj/gLD4/ygoKP+A
+ sPj/gLD4/ygoKP+AsPj/gLD4/ygoKP8oKCj/KCgo/wAAAAAAAAAAKCgo/ygoKP8oKCj/gLD4/4Cw+P8o
+ KCj/gLD4/4Cw+P8oKCj/gLD4/4Cw+P8oKCj/KCgo/ygoKP8AAAAAAAAAACgoKP8oKCj/KCgo/4Cw+P+A
+ sPj/KCgo/4Cw+P+AsPj/KCgo/4Cw+P+AsPj/KCgo/ygoKP8oKCj/AAAAACgoKP8AEKj/ABCo/wAQqP8o
+ KCj/KCgo/ygoKP8oKCj/KCgo/ygoKP8oKCj/KCgo/wAQqP8AEKj/ABCo/ygoKP8oKCj/AEao/wBGqP8A
+ Rqj/KCgo/ygoKP8oKCj/KCgo/ygoKP8oKCj/KCgo/ygoKP8ARqj/AEao/wBGqP8oKCj/KCgo/wCoqP8A
+ qKj/AKio/ygoKP8oKCj/KCgo/ygoKP8oKCj/KCgo/ygoKP8oKCj/AKio/wCoqP8AqKj/KCgo/ygoKP8A
+ EKj/GAjw/xgI8P8AEKj/ABCo//j4+P/4+Pj/+Pj4//j4+P8AEKj/ABCo/xgI8P8YCPD/ABCo/ygoKP8o
+ KCj/AEao/wdo8P8HaPD/AEao/wBGqP/4+Pj/+Pj4//j4+P/4+Pj/AEao/wBGqP8HaPD/B2jw/wBGqP8o
+ KCj/KCgo/wCoqP8H8PD/B/Dw/wCoqP8AqKj/+Pj4//j4+P/4+Pj/+Pj4/wCoqP8AqKj/B/Dw/wfw8P8A
+ qKj/KCgo/ygoKP/4+Pj/GAjw/xgI8P8YCPD/+Pj4//j4+P/4+Pj/+Pj4//j4+P/4+Pj/GAjw/xgI8P8Y
+ CPD/+Pj4/ygoKP8oKCj/+Pj4/wdo8P8HaPD/B2jw//j4+P/4+Pj/+Pj4//j4+P/4+Pj/+Pj4/wdo8P8H
+ aPD/B2jw//j4+P8oKCj/KCgo//j4+P8H8PD/B/Dw/wfw8P/4+Pj/+Pj4//j4+P/4+Pj/+Pj4//j4+P8H
+ 8PD/B/Dw/wfw8P/4+Pj/KCgo/ygoKP/4+Pj/+Pj4/xgI8P94YPj/+Pj4//j4+P/4+Pj/+Pj4//j4+P/4
+ +Pj/eGD4/xgI8P/4+Pj/+Pj4/ygoKP8oKCj/+Pj4//j4+P8HaPD/YJ/3//j4+P/4+Pj/+Pj4//j4+P/4
+ +Pj/+Pj4/2Cf9/8HaPD/+Pj4//j4+P8oKCj/KCgo//j4+P/4+Pj/B/Dw/2D39//4+Pj/+Pj4//j4+P/4
+ +Pj/+Pj4//j4+P9g9/f/B/Dw//j4+P/4+Pj/KCgo/ygoKP/4+Pj/+Pj4/xgI8P94YPj/+Pj4//j4+P/4
+ +Pj/+Pj4//j4+P/4+Pj/eGD4/xgI8P/4+Pj/+Pj4/ygoKP8oKCj/+Pj4//j4+P8HaPD/YJ/3//j4+P/4
+ +Pj/+Pj4//j4+P/4+Pj/+Pj4/2Cf9/8HaPD/+Pj4//j4+P8oKCj/KCgo//j4+P/4+Pj/B/Dw/2D39//4
+ +Pj/+Pj4//j4+P/4+Pj/+Pj4//j4+P9g9/f/B/Dw//j4+P/4+Pj/KCgo/ygoKP/4+Pj/GAjw/xgI8P94
+ YPj/eGD4//j4+P/4+Pj/+Pj4//j4+P94YPj/eGD4/xgI8P8YCPD/+Pj4/ygoKP8oKCj/+Pj4/wdo8P8H
+ aPD/YJ/3/2Cf9//4+Pj/+Pj4//j4+P/4+Pj/YJ/3/2Cf9/8HaPD/B2jw//j4+P8oKCj/KCgo//j4+P8H
+ 8PD/B/Dw/2D39/9g9/f/+Pj4//j4+P/4+Pj/+Pj4/2D39/9g9/f/B/Dw/wfw8P/4+Pj/KCgo/wAAAAAo
+ KCj/ABCo/xgI8P8YCPD/eGD4/3hg+P94YPj/eGD4/3hg+P94YPj/GAjw/xgI8P8AEKj/KCgo/wAAAAAA
+ AAAAKCgo/wBGqP8HaPD/B2jw/2Cf9/9gn/f/YJ/3/2Cf9/9gn/f/YJ/3/wdo8P8HaPD/AEao/ygoKP8A
+ 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
+ sPj/uND4//j4+P/4+Pj/+Pj4//j4+P/4+Pj/+Pj4//j4+P/4+Pj/+Pj4//j4+P/4+Pj/+Pj4//j4+P/4
+ +Pj/uND4/4Cw+P9QaJD/KCgo/ygoKH8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACgoKP8o
+ KCj/gLD4/4Cw+P/4+Pj/+Pj4//j4+P/4+Pj/+Pj4//j4+P/4+Pj/+Pj4//j4+P/4+Pj/+Pj4//j4+P/4
+ +Pj/+Pj4//j4+P/4+Pj/gLD4/4Cw+P8oKCj/KCgo/wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAKCgo/ygoKP+AsPj/gLD4//j4+P/4+Pj/+Pj4//j4+P/AwMD/wMDA//j4+P/4+Pj/+Pj4//j4+P/A
+ wMD/wMDA//j4+P/4+Pj/+Pj4//j4+P+AsPj/gLD4/ygoKP8oKCj/AAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAACgoKMEoKCj/KCgo/1BokP+AsPj/uND4//j4+P/4+Pj/+Pj4/ygoKP8oKCj/+Pj4//j4+P/4
+ +Pj/+Pj4/ygoKP8oKCj/+Pj4//j4+P/4+Pj/uND4/4Cw+P9QaJD/KCgo/ygoKP8oKCjBAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAoKCjBKCgo/ygoKP8oKCj/KCgo/1BokP+AsPj/gLD4/4Cw+P+AsPj/KCgo/ygoKP+A
+ sPj/gLD4/4Cw+P+AsPj/KCgo/ygoKP+AsPj/gLD4/4Cw+P+AsPj/UGiQ/ygoKP8oKCj/KCgo/ygoKP8o
+ KCjBAAAAAAAAAAAAAAAAKCgowSgoKP8oKCj/KCgo/ygoKP8oKCj/KCgo/zhIWP+AsPj/gLD4/1BokP8o
+ KCj/KCgo/1BokP+AsPj/gLD4/1BokP8oKCj/KCgo/1BokP+AsPj/gLD4/zhIWP8oKCj/KCgo/ygoKP8o
+ KCj/KCgo/ygoKP8oKCjBAAAAACgoKH8oKCj/EBho/wAQqP8AEKj/ABCo/wAQqP8YIEj/KCgo/ygoKP8o
+ KCj/KCgo/ygoKP8oKCj/KCgo/ygoKP8oKCj/KCgo/ygoKP8oKCj/KCgo/ygoKP8oKCj/KCgo/xggSP8A
+ EKj/ABCo/wAQqP8AEKj/EBho/ygoKP8oKCh/KCgo/ygoKP8AEKj/ABCo/wAQqP8AEKj/ABCo/wAQqP8I
+ EIj/KCgo/ygoKP8oKCj/KCgo/ygoKP8oKCj/KCgo/ygoKP8oKCj/KCgo/ygoKP8oKCj/KCgo/ygoKP8I
+ EIj/ABCo/wAQqP8AEKj/ABCo/wAQqP8AEKj/KCgo/ygoKP8oKCj/KCgo/wAQqP8AEKj/CAjI/xgI8P8Y
+ CPD/AAi4/wAQqP8AEKj/ABCo/zhIuP+4uOD/+Pj4//j4+P/4+Pj/+Pj4//j4+P/4+Pj/uLjg/zhIuP8A
+ EKj/ABCo/wAQqP8ACLj/GAjw/xgI8P8ICMj/ABCo/wAQqP8oKCj/KCgo/ygoKP8oKCj/ABCo/wAQqP8Y
+ CPD/GAjw/xgI8P8YCPD/CAjI/wAQqP8AEKj/eIDQ//j4+P/4+Pj/+Pj4//j4+P/4+Pj/+Pj4//j4+P/4
+ +Pj/eIDQ/wAQqP8AEKj/CAjI/xgI8P8YCPD/GAjw/xgI8P8AEKj/ABCo/ygoKP8oKCj/KCgo/ygoKP/4
+ +Pj/+Pj4/xgI8P8YCPD/GAjw/xgI8P8YCPD/UEDw/8C48P/4+Pj/+Pj4//j4+P/4+Pj/+Pj4//j4+P/4
+ +Pj/+Pj4//j4+P/4+Pj/wLjw/1BA8P8YCPD/GAjw/xgI8P8YCPD/GAjw//j4+P/4+Pj/KCgo/ygoKP8o
+ KCj/KCgo//j4+P/4+Pj/iIDw/xgI8P8YCPD/GAjw/xgI8P8YCPD/+Pj4//j4+P/4+Pj/+Pj4//j4+P/4
+ +Pj/+Pj4//j4+P/4+Pj/+Pj4//j4+P/4+Pj/GAjw/xgI8P8YCPD/GAjw/xgI8P+IgPD/+Pj4//j4+P8o
+ KCj/KCgo/ygoKP8oKCj/+Pj4//j4+P/4+Pj/UEDw/xgI8P8YCPD/SDDw/3hg+P/4+Pj/+Pj4//j4+P/4
+ +Pj/+Pj4//j4+P/4+Pj/+Pj4//j4+P/4+Pj/+Pj4//j4+P94YPj/SDDw/xgI8P8YCPD/UEDw//j4+P/4
+ +Pj/+Pj4/ygoKP8oKCj/KCgo/ygoKP/4+Pj/+Pj4//j4+P/4+Pj/GAjw/xgI8P94YPj/eGD4//j4+P/4
+ +Pj/+Pj4//j4+P/4+Pj/+Pj4//j4+P/4+Pj/+Pj4//j4+P/4+Pj/+Pj4/3hg+P94YPj/GAjw/xgI8P/4
+ +Pj/+Pj4//j4+P/4+Pj/KCgo/ygoKP8oKCj/KCgo//j4+P/4+Pj/+Pj4//j4+P8YCPD/GAjw/3hg+P94
+ YPj/+Pj4//j4+P/4+Pj/+Pj4//j4+P/4+Pj/+Pj4//j4+P/4+Pj/+Pj4//j4+P/4+Pj/eGD4/3hg+P8Y
+ CPD/GAjw//j4+P/4+Pj/+Pj4//j4+P8oKCj/KCgo/ygoKP8oKCj/+Pj4//j4+P/4+Pj/iIDw/xgI8P8Y
+ CPD/eGD4/3hg+P+YgPj/+Pj4//j4+P/4+Pj/+Pj4//j4+P/4+Pj/+Pj4//j4+P/4+Pj/+Pj4/5iA+P94
+ YPj/eGD4/xgI8P8YCPD/iIDw//j4+P/4+Pj/+Pj4/ygoKP8oKCj/KCgo/ygoKP/4+Pj/+Pj4/4iA8P8Y
+ CPD/GAjw/xgI8P94YPj/eGD4/3hg+P+4qPj/+Pj4//j4+P/4+Pj/+Pj4//j4+P/4+Pj/+Pj4//j4+P+4
+ qPj/eGD4/3hg+P94YPj/GAjw/xgI8P8YCPD/iIDw//j4+P/4+Pj/KCgo/ygoKP8oKCh/KCgo/5CQkP/A
+ uPD/UEDw/xgI8P8YCPD/GAjw/zAY8P94YPj/eGD4/3hg+P+YgPj/+Pj4//j4+P/4+Pj/+Pj4//j4+P/4
+ +Pj/mID4/3hg+P94YPj/eGD4/zAY8P8YCPD/GAjw/xgI8P9QQPD/wLjw/5CQkP8oKCj/KCgofwAAAAAo
+ KCjBKCgo/xggSP8IEIj/CAjI/xgI8P8YCPD/GAjw/0gw8P94YPj/eGD4/3hg+P94YPj/eGD4/3hg+P94
+ YPj/eGD4/3hg+P94YPj/eGD4/3hg+P9IMPD/GAjw/xgI8P8YCPD/CAjI/wgQiP8YIEj/KCgo/ygoKMEA
+ AAAAAAAAAAAAAAAoKCj/KCgo/wAQqP8AEKj/CAjI/xgI8P8YCPD/GAjw/0gw8P94YPj/eGD4/3hg+P94
+ YPj/eGD4/3hg+P94YPj/eGD4/3hg+P94YPj/SDDw/xgI8P8YCPD/GAjw/wgIyP8AEKj/ABCo/ygoKP8o
+ KCj/AAAAAAAAAAAAAAAAAAAAACgoKP8oKCj/ABCo/wAQqP8AEKj/CAjI/xgI8P8YCPD/GAjw/0gw8P94
+ YPj/eGD4/3hg+P94YPj/eGD4/3hg+P94YPj/eGD4/0gw8P8YCPD/GAjw/xgI8P8ICMj/ABCo/wAQqP8A
+ EKj/KCgo/ygoKP8AAAAAAAAAAAAAAAAAAAAAKCgofygoKP8QGGj/ABCo/wAQqP8ACLj/EAjY/xgI8P8Y
+ CPD/GAjw/zAY8P94YPj/eGD4/3hg+P94YPj/eGD4/3hg+P8wGPD/GAjw/xgI8P8YCPD/EAjY/wAIuP8A
+ EKj/ABCo/xAYaP8oKCj/KCgofwAAAAAAAAAAAAAAAAAAAAAAAAAAKCgowSgoKP9YWFj/wMDA//j4+P/4
+ +Pj/+Pj4//j4+P9QQPD/GAjw/xgI8P8YCPD/GAjw/xgI8P8YCPD/GAjw/xgI8P9QQPD/+Pj4//j4+P/4
+ +Pj/+Pj4/8DAwP9YWFj/KCgo/ygoKMEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKCgowSgoKP+Q
+ kJD/+Pj4//j4+P/4+Pj/+Pj4//j4+P+IgPD/GAjw/xgI8P8YCPD/GAjw/xgI8P8YCPD/iIDw//j4+P/4
+ +Pj/+Pj4//j4+P/4+Pj/kJCQ/ygoKP8oKCjBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAKCgowSgoKP8oKCj/KCgo/8DAwP/4+Pj/+Pj4//j4+P+4uOD/OEi4/wAQqP8AEKj/OEi4/7i44P/4
+ +Pj/+Pj4//j4+P/AwMD/KCgo/ygoKP8oKCj/KCgowQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAKCgofygoKP8oKCj/KCgo/5CQkP/4+Pj/+Pj4/7i44P84SLj/ABCo/wAQqP84
+ SLj/uLjg//j4+P/4+Pj/kJCQ/ygoKP8oKCj/KCgo/ygoKH8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAoKCjBKCgo/ygoKP8oKCj/KCgo/ygoKP8o
+ KCj/KCgo/ygoKP8oKCj/KCgo/ygoKP8oKCj/KCgowQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAoKCh/KCgo/ygoKP8o
+ KCj/KCgo/ygoKP8oKCj/KCgo/ygoKP8oKCj/KCgo/ygoKH8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAA/wAA//4AAH/wAAA/8AAAH/AAAA/wAAAP8AAAD+AAAAfAAAADgAAAAQAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAHAAAADwAAAA8AAAAPg
+ AAAH8AAAD/gAAB/8AAA//4AB///AA/8oAAAAEAAAACAAAAABACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAACgoKP8oKCj/KCgo/ygoKP8oKCj/KCgo/ygoKP8oKCj/AAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAKCgo/ygoKP+AsPj/+Pj4//j4+P/4+Pj/+Pj4//j4+P/4+Pj/gLD4/ygoKP8A
+ AAAAAAAAAAAAAAAAAAAAAAAAACgoKP+AsPj/+Pj4//j4+P/4+Pj/+Pj4//j4+P/4+Pj/+Pj4//j4+P+A
+ sPj/KCgo/wAAAAAAAAAAAAAAAAAAAAAoKCj/gLD4//j4+P/4+Pj/KCgo//j4+P/4+Pj/KCgo//j4+P/4
+ +Pj/gLD4/ygoKP8AAAAAAAAAAAAAAAAoKCj/KCgo/ygoKP+AsPj/gLD4/ygoKP+AsPj/gLD4/ygoKP+A
+ sPj/gLD4/ygoKP8oKCj/KCgo/wAAAAAoKCj/ABCo/wAQqP8AEKj/KCgo/ygoKP8oKCj/KCgo/ygoKP8o
+ KCj/KCgo/ygoKP8AEKj/ABCo/wAQqP8oKCj/KCgo/wAQqP8YCPD/GAjw/wAQqP8AEKj/+Pj4//j4+P/4
+ +Pj/+Pj4/wAQqP8AEKj/GAjw/xgI8P8AEKj/KCgo/ygoKP/4+Pj/GAjw/xgI8P8YCPD/+Pj4//j4+P/4
+ +Pj/+Pj4//j4+P/4+Pj/GAjw/xgI8P8YCPD/+Pj4/ygoKP8oKCj/+Pj4//j4+P8YCPD/eGD4//j4+P/4
+ +Pj/+Pj4//j4+P/4+Pj/+Pj4/3hg+P8YCPD/+Pj4//j4+P8oKCj/KCgo//j4+P/4+Pj/GAjw/3hg+P/4
+ +Pj/+Pj4//j4+P/4+Pj/+Pj4//j4+P94YPj/GAjw//j4+P/4+Pj/KCgo/ygoKP/4+Pj/GAjw/xgI8P94
+ YPj/eGD4//j4+P/4+Pj/+Pj4//j4+P94YPj/eGD4/xgI8P8YCPD/+Pj4/ygoKP8AAAAAKCgo/wAQqP8Y
+ CPD/GAjw/3hg+P94YPj/eGD4/3hg+P94YPj/eGD4/xgI8P8YCPD/ABCo/ygoKP8AAAAAAAAAACgoKP8A
+ EKj/ABCo/xgI8P8YCPD/eGD4/3hg+P94YPj/eGD4/xgI8P8YCPD/ABCo/wAQqP8oKCj/AAAAAAAAAAAA
+ AAAAKCgo//j4+P/4+Pj/+Pj4/xgI8P8YCPD/GAjw/xgI8P/4+Pj/+Pj4//j4+P8oKCj/AAAAAAAAAAAA
+ AAAAAAAAAAAAAAAoKCj/KCgo//j4+P/4+Pj/ABCo/wAQqP/4+Pj/+Pj4/ygoKP8oKCj/AAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAoKCj/KCgo/ygoKP8oKCj/KCgo/ygoKP8AAAAAAAAAAAAAAAAA
+ AAAAAAAAAPAPrEHAB6xBwAOsQcADrEGAAaxBAACsQQAArEEAAKxBAACsQQAArEEAAKxBgAGsQYABrEHA
+ A6xB4AesQfgfrEE=
+
+
+
+ 683, 17
+
+
+ 821, 17
+
+
+ 954, 17
+
+
+ 1143, 17
+
+
+ 1309, 17
+
+
+ 17, 58
+
+
+ 17, 17
+
+
+ 224, 58
+
+
+ 376, 58
+
+
\ No newline at end of file
diff --git a/src/Brutario.Win/Program.cs b/src/Brutario.Win/Program.cs
new file mode 100644
index 0000000..2d8f563
--- /dev/null
+++ b/src/Brutario.Win/Program.cs
@@ -0,0 +1,44 @@
+//
+// Copyright (c) 2022 spel werdz rite. All rights reserved. Licensed under GNU Affero
+// General Public License. See LICENSE in project root for full license
+// information, or visit https://www.gnu.org/licenses/#AGPL
+//
+
+namespace Brutario.Win;
+
+using System;
+using System.Windows.Forms;
+
+using Core;
+
+internal static class Program
+{
+ ///
+ /// The main entry point for the application.
+ ///
+ [STAThread]
+ private static void Main(string[] args)
+ {
+ Application.EnableVisualStyles();
+ Application.SetCompatibleTextRenderingDefault(false);
+
+ var editor = new BrutarioEditor();
+ using var form = new MainForm(editor);
+
+ // TODO(nrg): This probably does not belong here. The Form.Load event is already
+ // used internally, so there are no order guarantees. I could make a new event
+ // called within Form.Load and hook to that here. Ideally, I don't think the UI
+ // should be capturing command-line logic though. Perhaps a presenter `Ready`
+ // event is the way to go. Another caveat is that I want Presenter.Open, not
+ // editor.Open. Originally, Presenter was private in MainForm and is now public
+ // only for this.
+ if (args.Length == 1)
+ {
+ form.Load += (s, e) => form.Presenter.Open(args[0]);
+ }
+
+ // TODO(nrg): In a similar note to the above comment, perhaps the presenter
+ // should handle the run logic? Not really sure.
+ Application.Run(form);
+ }
+}
diff --git a/src/Brutario.Win/Properties/Resources.Designer.cs b/src/Brutario.Win/Properties/Resources.Designer.cs
new file mode 100644
index 0000000..792d5ab
--- /dev/null
+++ b/src/Brutario.Win/Properties/Resources.Designer.cs
@@ -0,0 +1,818 @@
+//------------------------------------------------------------------------------
+//
+// This code was generated by a tool.
+// Runtime Version:4.0.30319.42000
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+//
+//------------------------------------------------------------------------------
+
+namespace Brutario.Win.Properties {
+ using System;
+
+
+ ///
+ /// A strongly-typed resource class, for looking up localized strings, etc.
+ ///
+ // This class was auto-generated by the StronglyTypedResourceBuilder
+ // class via a tool like ResGen or Visual Studio.
+ // To add or remove a member, edit your .ResX file then rerun ResGen
+ // with the /str option, or rebuild your VS project.
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")]
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+ [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+ internal class Resources {
+
+ private static global::System.Resources.ResourceManager resourceMan;
+
+ private static global::System.Globalization.CultureInfo resourceCulture;
+
+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+ internal Resources() {
+ }
+
+ ///
+ /// Returns the cached ResourceManager instance used by this class.
+ ///
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Resources.ResourceManager ResourceManager {
+ get {
+ if (object.ReferenceEquals(resourceMan, null)) {
+ global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Brutario.Win.Properties.Resources", typeof(Resources).Assembly);
+ resourceMan = temp;
+ }
+ return resourceMan;
+ }
+ }
+
+ ///
+ /// Overrides the current thread's CurrentUICulture property for all
+ /// resource lookups using this strongly typed resource class.
+ ///
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Globalization.CultureInfo Culture {
+ get {
+ return resourceCulture;
+ }
+ set {
+ resourceCulture = value;
+ }
+ }
+
+ ///
+ /// Looks up a localized resource of type System.Drawing.Bitmap.
+ ///
+ internal static System.Drawing.Bitmap alien {
+ get {
+ object obj = ResourceManager.GetObject("alien", resourceCulture);
+ return ((System.Drawing.Bitmap)(obj));
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Flag Pole.
+ ///
+ internal static string AltFlagPole {
+ get {
+ return ResourceManager.GetString("AltFlagPole", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to J-Pipe.
+ ///
+ internal static string AltJPipe {
+ get {
+ return ResourceManager.GetString("AltJPipe", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Scroll Stop.
+ ///
+ internal static string AltScrollStop {
+ get {
+ return ResourceManager.GetString("AltScrollStop", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Bullet Bill Shooter (Height={0}).
+ ///
+ internal static string AreaSpecificPlatform_BulletBillTurrets {
+ get {
+ return ResourceManager.GetString("AreaSpecificPlatform_BulletBillTurrets", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Cloud Ground (Width={0}).
+ ///
+ internal static string AreaSpecificPlatform_CloudGround {
+ get {
+ return ResourceManager.GetString("AreaSpecificPlatform_CloudGround", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Mushroom Platform (Width={0}).
+ ///
+ internal static string AreaSpecificPlatform_Mushrooms {
+ get {
+ return ResourceManager.GetString("AreaSpecificPlatform_Mushrooms", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Tree Top Platform (Width={0}).
+ ///
+ internal static string AreaSpecificPlatform_Trees {
+ get {
+ return ResourceManager.GetString("AreaSpecificPlatform_Trees", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Pulley Platforms (Width={0}).
+ ///
+ internal static string BalanceHorizontalRope {
+ get {
+ return ResourceManager.GetString("BalanceHorizontalRope", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Bowser Axe.
+ ///
+ internal static string BowserAxe {
+ get {
+ return ResourceManager.GetString("BowserAxe", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Bowser Bridge.
+ ///
+ internal static string BowserBridge {
+ get {
+ return ResourceManager.GetString("BowserBridge", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Brick (10 Coins).
+ ///
+ internal static string Brick10Coins {
+ get {
+ return ResourceManager.GetString("Brick10Coins", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Brick (1UP).
+ ///
+ internal static string Brick1UP {
+ get {
+ return ResourceManager.GetString("Brick1UP", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Brick and Scenery Change.
+ ///
+ internal static string BrickAndSceneryChange {
+ get {
+ return ResourceManager.GetString("BrickAndSceneryChange", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Brick (Beanstalk).
+ ///
+ internal static string BrickBeanstalk {
+ get {
+ return ResourceManager.GetString("BrickBeanstalk", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Brick (Powerup).
+ ///
+ internal static string BrickPowerup {
+ get {
+ return ResourceManager.GetString("BrickPowerup", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Brick (Star).
+ ///
+ internal static string BrickStar {
+ get {
+ return ResourceManager.GetString("BrickStar", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Rope Bridge (Y=10, Width={0}).
+ ///
+ internal static string BridgeV10 {
+ get {
+ return ResourceManager.GetString("BridgeV10", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Rope Bridge (Y=7, Width={0}).
+ ///
+ internal static string BridgeV7 {
+ get {
+ return ResourceManager.GetString("BridgeV7", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Rope Bridge (Y=8, Width={0}).
+ ///
+ internal static string BridgeV8 {
+ get {
+ return ResourceManager.GetString("BridgeV8", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Generator: Bullet Bills.
+ ///
+ internal static string BulletBillGenerator {
+ get {
+ return ResourceManager.GetString("BulletBillGenerator", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Castle.
+ ///
+ internal static string Castle {
+ get {
+ return ResourceManager.GetString("Castle", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Castle Object: Ceiling Cap Tile.
+ ///
+ internal static string CastleCeilingCap {
+ get {
+ return ResourceManager.GetString("CastleCeilingCap", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Castle Object: Left-Facing Wall To Floor.
+ ///
+ internal static string CastleFloorLeftEdge {
+ get {
+ return ResourceManager.GetString("CastleFloorLeftEdge", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Castle Object: Left-Facing Wall.
+ ///
+ internal static string CastleFloorLeftWall {
+ get {
+ return ResourceManager.GetString("CastleFloorLeftWall", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Castle Object: Right-Facing Wall To Floor.
+ ///
+ internal static string CastleFloorRightEdge {
+ get {
+ return ResourceManager.GetString("CastleFloorRightEdge", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Castle Object: Right-Facing Wall.
+ ///
+ internal static string CastleFloorRightWall {
+ get {
+ return ResourceManager.GetString("CastleFloorRightWall", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Castle Object: Rectangular Ceiling Tiles.
+ ///
+ internal static string CastleRectangularCeilingTiles {
+ get {
+ return ResourceManager.GetString("CastleRectangularCeilingTiles", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Castle Object: Descending Stairs.
+ ///
+ internal static string CastleStairs {
+ get {
+ return ResourceManager.GetString("CastleStairs", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized resource of type System.Drawing.Bitmap.
+ ///
+ internal static System.Drawing.Bitmap circle_question_regular {
+ get {
+ object obj = ResourceManager.GetObject("circle-question-regular", resourceCulture);
+ return ((System.Drawing.Bitmap)(obj));
+ }
+ }
+
+ ///
+ /// Looks up a localized resource of type System.Drawing.Bitmap.
+ ///
+ internal static System.Drawing.Bitmap copy_solid {
+ get {
+ object obj = ResourceManager.GetObject("copy-solid", resourceCulture);
+ return ((System.Drawing.Bitmap)(obj));
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Nothing.
+ ///
+ internal static string Empty {
+ get {
+ return ResourceManager.GetString("Empty", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Nothing.
+ ///
+ internal static string Empty2 {
+ get {
+ return ResourceManager.GetString("Empty2", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Empty Tile.
+ ///
+ internal static string EmptyTile {
+ get {
+ return ResourceManager.GetString("EmptyTile", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Enterable Pipe (Height={0}).
+ ///
+ internal static string EnterablePipe {
+ get {
+ return ResourceManager.GetString("EnterablePipe", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Extendable J-Pipe (Height={0}).
+ ///
+ internal static string ExtendableJPipe {
+ get {
+ return ResourceManager.GetString("ExtendableJPipe", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Flag Pole.
+ ///
+ internal static string FlagPole {
+ get {
+ return ResourceManager.GetString("FlagPole", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized resource of type System.Drawing.Bitmap.
+ ///
+ internal static System.Drawing.Bitmap floppy_disk_regular {
+ get {
+ object obj = ResourceManager.GetObject("floppy-disk-regular", resourceCulture);
+ return ((System.Drawing.Bitmap)(obj));
+ }
+ }
+
+ ///
+ /// Looks up a localized resource of type System.Drawing.Bitmap.
+ ///
+ internal static System.Drawing.Bitmap folder_open_solid {
+ get {
+ object obj = ResourceManager.GetObject("folder-open-solid", resourceCulture);
+ return ((System.Drawing.Bitmap)(obj));
+ }
+ }
+
+ ///
+ /// Looks up a localized resource of type System.Drawing.Bitmap.
+ ///
+ internal static System.Drawing.Bitmap folder_tree_solid {
+ get {
+ object obj = ResourceManager.GetObject("folder-tree-solid", resourceCulture);
+ return ((System.Drawing.Bitmap)(obj));
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Foreground Change.
+ ///
+ internal static string ForegroundChange {
+ get {
+ return ResourceManager.GetString("ForegroundChange", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized resource of type System.Drawing.Bitmap.
+ ///
+ internal static System.Drawing.Bitmap hands_clapping_solid {
+ get {
+ object obj = ResourceManager.GetObject("hands-clapping-solid", resourceCulture);
+ return ((System.Drawing.Bitmap)(obj));
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Hidden Block (1UP).
+ ///
+ internal static string HiddenBlock1UP {
+ get {
+ return ResourceManager.GetString("HiddenBlock1UP", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Hidden Block (Coin).
+ ///
+ internal static string HiddenBlockCoin {
+ get {
+ return ResourceManager.GetString("HiddenBlockCoin", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Hole (Width={0}).
+ ///
+ internal static string Hole {
+ get {
+ return ResourceManager.GetString("Hole", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Hole with water or lava (Width={0}).
+ ///
+ internal static string HoleWithWaterOrLava {
+ get {
+ return ResourceManager.GetString("HoleWithWaterOrLava", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Horizontal Bricks (Width={0}).
+ ///
+ internal static string HorizontalBricks {
+ get {
+ return ResourceManager.GetString("HorizontalBricks", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Horizontal Coins (Width={0}).
+ ///
+ internal static string HorizontalCoins {
+ get {
+ return ResourceManager.GetString("HorizontalCoins", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Row of Coin Blocks (Y=3, Width={0}).
+ ///
+ internal static string HorizontalQuestionBlocksV3 {
+ get {
+ return ResourceManager.GetString("HorizontalQuestionBlocksV3", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Row of Coin Blocks (Y=7, Width={0}).
+ ///
+ internal static string HorizontalQuestionBlocksV7 {
+ get {
+ return ResourceManager.GetString("HorizontalQuestionBlocksV7", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Horizontal Blocks (Width={0}).
+ ///
+ internal static string HorizontalStones {
+ get {
+ return ResourceManager.GetString("HorizontalStones", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to J-Pipe.
+ ///
+ internal static string JPipe {
+ get {
+ return ResourceManager.GetString("JPipe", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Screen Loop Command.
+ ///
+ internal static string LoopCommand {
+ get {
+ return ResourceManager.GetString("LoopCommand", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized resource of type System.Drawing.Bitmap.
+ ///
+ internal static System.Drawing.Bitmap map_regular {
+ get {
+ object obj = ResourceManager.GetObject("map-regular", resourceCulture);
+ return ((System.Drawing.Bitmap)(obj));
+ }
+ }
+
+ ///
+ /// Looks up a localized resource of type System.Drawing.Bitmap.
+ ///
+ internal static System.Drawing.Bitmap minus_solid {
+ get {
+ object obj = ResourceManager.GetObject("minus-solid", resourceCulture);
+ return ((System.Drawing.Bitmap)(obj));
+ }
+ }
+
+ ///
+ /// Looks up a localized resource of type System.Drawing.Bitmap.
+ ///
+ internal static System.Drawing.Bitmap paste_solid {
+ get {
+ object obj = ResourceManager.GetObject("paste-solid", resourceCulture);
+ return ((System.Drawing.Bitmap)(obj));
+ }
+ }
+
+ ///
+ /// Looks up a localized resource of type System.Drawing.Bitmap.
+ ///
+ internal static System.Drawing.Bitmap Picture1 {
+ get {
+ object obj = ResourceManager.GetObject("Picture1", resourceCulture);
+ return ((System.Drawing.Bitmap)(obj));
+ }
+ }
+
+ ///
+ /// Looks up a localized resource of type System.Drawing.Bitmap.
+ ///
+ internal static System.Drawing.Bitmap Picture2 {
+ get {
+ object obj = ResourceManager.GetObject("Picture2", resourceCulture);
+ return ((System.Drawing.Bitmap)(obj));
+ }
+ }
+
+ ///
+ /// Looks up a localized resource of type System.Drawing.Bitmap.
+ ///
+ internal static System.Drawing.Bitmap plus_solid {
+ get {
+ object obj = ResourceManager.GetObject("plus-solid", resourceCulture);
+ return ((System.Drawing.Bitmap)(obj));
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Rope for pulley platforms (Height={0}).
+ ///
+ internal static string PulleyRope {
+ get {
+ return ResourceManager.GetString("PulleyRope", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Question Block (Coin).
+ ///
+ internal static string QuestionBlockCoin {
+ get {
+ return ResourceManager.GetString("QuestionBlockCoin", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Question Block (Powerup).
+ ///
+ internal static string QuestionBlockPowerup {
+ get {
+ return ResourceManager.GetString("QuestionBlockPowerup", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Generator: Red flying cheep-cheeps.
+ ///
+ internal static string RedCheepCheepFlying {
+ get {
+ return ResourceManager.GetString("RedCheepCheepFlying", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Rope for platform lifts.
+ ///
+ internal static string RopeForLift {
+ get {
+ return ResourceManager.GetString("RopeForLift", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized resource of type System.Drawing.Bitmap.
+ ///
+ internal static System.Drawing.Bitmap rotate_left_solid {
+ get {
+ object obj = ResourceManager.GetObject("rotate-left-solid", resourceCulture);
+ return ((System.Drawing.Bitmap)(obj));
+ }
+ }
+
+ ///
+ /// Looks up a localized resource of type System.Drawing.Bitmap.
+ ///
+ internal static System.Drawing.Bitmap rotate_right_solid {
+ get {
+ object obj = ResourceManager.GetObject("rotate-right-solid", resourceCulture);
+ return ((System.Drawing.Bitmap)(obj));
+ }
+ }
+
+ ///
+ /// Looks up a localized resource of type System.Drawing.Bitmap.
+ ///
+ internal static System.Drawing.Bitmap scissors_solid {
+ get {
+ object obj = ResourceManager.GetObject("scissors-solid", resourceCulture);
+ return ((System.Drawing.Bitmap)(obj));
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Skip to screen 0x{1:X2}.
+ ///
+ internal static string ScreenJump {
+ get {
+ return ResourceManager.GetString("ScreenJump", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Scroll Stop.
+ ///
+ internal static string ScrollStop {
+ get {
+ return ResourceManager.GetString("ScrollStop", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Scroll Stop (Warp Zone).
+ ///
+ internal static string ScrollStopWarpZone {
+ get {
+ return ResourceManager.GetString("ScrollStopWarpZone", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Sideways Pipe.
+ ///
+ internal static string SidewaysPipe {
+ get {
+ return ResourceManager.GetString("SidewaysPipe", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Spring Board.
+ ///
+ internal static string SpringBoard {
+ get {
+ return ResourceManager.GetString("SpringBoard", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Staircase (Width={0}).
+ ///
+ internal static string Staircase {
+ get {
+ return ResourceManager.GetString("Staircase", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Stop Generator (also stops Lakitus).
+ ///
+ internal static string StopGenerator {
+ get {
+ return ResourceManager.GetString("StopGenerator", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized resource of type System.Drawing.Bitmap.
+ ///
+ internal static System.Drawing.Bitmap trash_solid {
+ get {
+ object obj = ResourceManager.GetObject("trash-solid", resourceCulture);
+ return ((System.Drawing.Bitmap)(obj));
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Unenterable Pipe (Height={0}).
+ ///
+ internal static string UnenterablePipe {
+ get {
+ return ResourceManager.GetString("UnenterablePipe", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Unknown command: {2}.
+ ///
+ internal static string UnknownCommand {
+ get {
+ return ResourceManager.GetString("UnknownCommand", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Used Block.
+ ///
+ internal static string UsedBlock {
+ get {
+ return ResourceManager.GetString("UsedBlock", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Vertical Climbing Balls (Height={0}).
+ ///
+ internal static string VerticalBalls {
+ get {
+ return ResourceManager.GetString("VerticalBalls", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Vertical Bricks (Height={0}).
+ ///
+ internal static string VerticalBricks {
+ get {
+ return ResourceManager.GetString("VerticalBricks", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Vertical Sea Blocks (Height={0}).
+ ///
+ internal static string VerticalSeaBlocks {
+ get {
+ return ResourceManager.GetString("VerticalSeaBlocks", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Vertical Blocks (Height={0}).
+ ///
+ internal static string VerticalStones {
+ get {
+ return ResourceManager.GetString("VerticalStones", resourceCulture);
+ }
+ }
+ }
+}
diff --git a/src/Brutario.Win/Properties/Resources.resx b/src/Brutario.Win/Properties/Resources.resx
new file mode 100644
index 0000000..3d7ebad
--- /dev/null
+++ b/src/Brutario.Win/Properties/Resources.resx
@@ -0,0 +1,367 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+
+ ..\Resources\alien.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
+
+
+ ..\Resources\paste-solid.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
+
+
+ ..\Resources\folder-tree-solid.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
+
+
+ ..\Resources\plus-solid.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
+
+
+ ..\Resources\circle-question-regular.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
+
+
+ ..\Resources\minus-solid.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
+
+
+ ..\Resources\rotate-right-solid.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
+
+
+ ..\Resources\folder-open-solid.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
+
+
+ ..\Resources\trash-solid.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
+
+
+ ..\Resources\hands-clapping-solid.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
+
+
+ ..\Resources\copy-solid.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
+
+
+ ..\Resources\floppy-disk-regular.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
+
+
+ ..\Resources\scissors-solid.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
+
+
+ ..\Resources\Picture1.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
+
+
+ ..\Resources\rotate-left-solid.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
+
+
+ ..\Resources\map-regular.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
+
+
+ ..\Resources\Picture2.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
+
+
+ Flag Pole
+
+
+ J-Pipe
+
+
+ Scroll Stop
+
+
+ Bullet Bill Shooter (Height={0})
+
+
+ Cloud Ground (Width={0})
+
+
+ Mushroom Platform (Width={0})
+
+
+ Tree Top Platform (Width={0})
+
+
+ Pulley Platforms (Width={0})
+
+
+ Bowser Axe
+
+
+ Bowser Bridge
+
+
+ Brick (10 Coins)
+
+
+ Brick (1UP)
+
+
+ Brick and Scenery Change
+
+
+ Brick (Beanstalk)
+
+
+ Brick (Powerup)
+
+
+ Brick (Star)
+
+
+ Rope Bridge (Y=10, Width={0})
+
+
+ Rope Bridge (Y=7, Width={0})
+
+
+ Rope Bridge (Y=8, Width={0})
+
+
+ Generator: Bullet Bills
+
+
+ Castle
+
+
+ Castle Object: Ceiling Cap Tile
+
+
+ Castle Object: Left-Facing Wall To Floor
+
+
+ Castle Object: Left-Facing Wall
+
+
+ Castle Object: Right-Facing Wall To Floor
+
+
+ Castle Object: Right-Facing Wall
+
+
+ Castle Object: Rectangular Ceiling Tiles
+
+
+ Castle Object: Descending Stairs
+
+
+ Nothing
+
+
+ Nothing
+
+
+ Empty Tile
+
+
+ Enterable Pipe (Height={0})
+
+
+ Extendable J-Pipe (Height={0})
+
+
+ Flag Pole
+
+
+ Foreground Change
+
+
+ Hidden Block (1UP)
+
+
+ Hidden Block (Coin)
+
+
+ Hole (Width={0})
+
+
+ Hole with water or lava (Width={0})
+
+
+ Horizontal Bricks (Width={0})
+
+
+ Horizontal Coins (Width={0})
+
+
+ Row of Coin Blocks (Y=3, Width={0})
+
+
+ Row of Coin Blocks (Y=7, Width={0})
+
+
+ Horizontal Blocks (Width={0})
+
+
+ J-Pipe
+
+
+ Screen Loop Command
+
+
+ Rope for pulley platforms (Height={0})
+
+
+ Question Block (Coin)
+
+
+ Question Block (Powerup)
+
+
+ Generator: Red flying cheep-cheeps
+
+
+ Rope for platform lifts
+
+
+ Skip to screen 0x{1:X2}
+
+
+ Scroll Stop
+
+
+ Scroll Stop (Warp Zone)
+
+
+ Sideways Pipe
+
+
+ Spring Board
+
+
+ Staircase (Width={0})
+
+
+ Stop Generator (also stops Lakitus)
+
+
+ Unenterable Pipe (Height={0})
+
+
+ Unknown command: {2}
+
+
+ Used Block
+
+
+ Vertical Climbing Balls (Height={0})
+
+
+ Vertical Bricks (Height={0})
+
+
+ Vertical Sea Blocks (Height={0})
+
+
+ Vertical Blocks (Height={0})
+
+
\ No newline at end of file
diff --git a/src/Brutario.Win/Properties/Settings.Designer.cs b/src/Brutario.Win/Properties/Settings.Designer.cs
new file mode 100644
index 0000000..efdc8f3
--- /dev/null
+++ b/src/Brutario.Win/Properties/Settings.Designer.cs
@@ -0,0 +1,86 @@
+//------------------------------------------------------------------------------
+//
+// This code was generated by a tool.
+// Runtime Version:4.0.30319.42000
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+//
+//------------------------------------------------------------------------------
+
+namespace Brutario.Win.Properties {
+
+
+ [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "17.11.0.0")]
+ internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase {
+
+ private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings())));
+
+ public static Settings Default {
+ get {
+ return defaultInstance;
+ }
+ }
+
+ [global::System.Configuration.UserScopedSettingAttribute()]
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+ [global::System.Configuration.DefaultSettingValueAttribute("True")]
+ public bool AutoSaveEnabled {
+ get {
+ return ((bool)(this["AutoSaveEnabled"]));
+ }
+ set {
+ this["AutoSaveEnabled"] = value;
+ }
+ }
+
+ [global::System.Configuration.UserScopedSettingAttribute()]
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+ [global::System.Configuration.DefaultSettingValueAttribute("00:05:00")]
+ public global::System.TimeSpan AutoSaveInterval {
+ get {
+ return ((global::System.TimeSpan)(this["AutoSaveInterval"]));
+ }
+ set {
+ this["AutoSaveInterval"] = value;
+ }
+ }
+
+ [global::System.Configuration.UserScopedSettingAttribute()]
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+ [global::System.Configuration.DefaultSettingValueAttribute("04:00:00")]
+ public global::System.TimeSpan AutoSavePruningCutoff {
+ get {
+ return ((global::System.TimeSpan)(this["AutoSavePruningCutoff"]));
+ }
+ set {
+ this["AutoSavePruningCutoff"] = value;
+ }
+ }
+
+ [global::System.Configuration.UserScopedSettingAttribute()]
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+ [global::System.Configuration.DefaultSettingValueAttribute("False")]
+ public bool AutoSaveHardCutoff {
+ get {
+ return ((bool)(this["AutoSaveHardCutoff"]));
+ }
+ set {
+ this["AutoSaveHardCutoff"] = value;
+ }
+ }
+
+ [global::System.Configuration.UserScopedSettingAttribute()]
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+ [global::System.Configuration.DefaultSettingValueAttribute("True")]
+ public bool AutoSavePruningEnabled {
+ get {
+ return ((bool)(this["AutoSavePruningEnabled"]));
+ }
+ set {
+ this["AutoSavePruningEnabled"] = value;
+ }
+ }
+ }
+}
diff --git a/src/Brutario.Win/Properties/Settings.settings b/src/Brutario.Win/Properties/Settings.settings
new file mode 100644
index 0000000..518ff3c
--- /dev/null
+++ b/src/Brutario.Win/Properties/Settings.settings
@@ -0,0 +1,21 @@
+
+
+
+
+
+ True
+
+
+ 00:05:00
+
+
+ 04:00:00
+
+
+ False
+
+
+ True
+
+
+
\ No newline at end of file
diff --git a/src/Brutario.Win/Properties/launchSettings.json b/src/Brutario.Win/Properties/launchSettings.json
new file mode 100644
index 0000000..dc7bd49
--- /dev/null
+++ b/src/Brutario.Win/Properties/launchSettings.json
@@ -0,0 +1,8 @@
+{
+ "profiles": {
+ "Brutario.Win": {
+ "commandName": "Project",
+ "commandLineArgs": "\"C:\\Users\\spel werdz rite\\Dropbox\\Nelson\\Desktop\\Super Mario All-Stars (U) [!].sfc\""
+ }
+ }
+}
diff --git a/src/Brutario.Win/Resources/Picture1.png b/src/Brutario.Win/Resources/Picture1.png
new file mode 100644
index 0000000..879ebae
Binary files /dev/null and b/src/Brutario.Win/Resources/Picture1.png differ
diff --git a/src/Brutario.Win/Resources/Picture2.png b/src/Brutario.Win/Resources/Picture2.png
new file mode 100644
index 0000000..f16aa4d
Binary files /dev/null and b/src/Brutario.Win/Resources/Picture2.png differ
diff --git a/src/Brutario.Win/Resources/alien.png b/src/Brutario.Win/Resources/alien.png
new file mode 100644
index 0000000..b1dd3e9
Binary files /dev/null and b/src/Brutario.Win/Resources/alien.png differ
diff --git a/src/Brutario.Win/Resources/circle-question-regular.png b/src/Brutario.Win/Resources/circle-question-regular.png
new file mode 100644
index 0000000..d89dc3c
Binary files /dev/null and b/src/Brutario.Win/Resources/circle-question-regular.png differ
diff --git a/src/Brutario.Win/Resources/copy-solid.png b/src/Brutario.Win/Resources/copy-solid.png
new file mode 100644
index 0000000..30e2ec7
Binary files /dev/null and b/src/Brutario.Win/Resources/copy-solid.png differ
diff --git a/src/Brutario.Win/Resources/floppy-disk-regular.png b/src/Brutario.Win/Resources/floppy-disk-regular.png
new file mode 100644
index 0000000..0dc25bc
Binary files /dev/null and b/src/Brutario.Win/Resources/floppy-disk-regular.png differ
diff --git a/src/Brutario.Win/Resources/folder-open-solid.png b/src/Brutario.Win/Resources/folder-open-solid.png
new file mode 100644
index 0000000..6a491aa
Binary files /dev/null and b/src/Brutario.Win/Resources/folder-open-solid.png differ
diff --git a/src/Brutario.Win/Resources/folder-tree-solid.png b/src/Brutario.Win/Resources/folder-tree-solid.png
new file mode 100644
index 0000000..27e2293
Binary files /dev/null and b/src/Brutario.Win/Resources/folder-tree-solid.png differ
diff --git a/src/Brutario.Win/Resources/hands-clapping-solid.png b/src/Brutario.Win/Resources/hands-clapping-solid.png
new file mode 100644
index 0000000..3f2ca04
Binary files /dev/null and b/src/Brutario.Win/Resources/hands-clapping-solid.png differ
diff --git a/src/Brutario.Win/Resources/map-regular.png b/src/Brutario.Win/Resources/map-regular.png
new file mode 100644
index 0000000..d5a62af
Binary files /dev/null and b/src/Brutario.Win/Resources/map-regular.png differ
diff --git a/src/Brutario.Win/Resources/minus-solid.png b/src/Brutario.Win/Resources/minus-solid.png
new file mode 100644
index 0000000..5d9835d
Binary files /dev/null and b/src/Brutario.Win/Resources/minus-solid.png differ
diff --git a/src/Brutario.Win/Resources/paste-solid.png b/src/Brutario.Win/Resources/paste-solid.png
new file mode 100644
index 0000000..905d165
Binary files /dev/null and b/src/Brutario.Win/Resources/paste-solid.png differ
diff --git a/src/Brutario.Win/Resources/plus-solid.png b/src/Brutario.Win/Resources/plus-solid.png
new file mode 100644
index 0000000..c126297
Binary files /dev/null and b/src/Brutario.Win/Resources/plus-solid.png differ
diff --git a/src/Brutario.Win/Resources/rotate-left-solid.png b/src/Brutario.Win/Resources/rotate-left-solid.png
new file mode 100644
index 0000000..7b7afca
Binary files /dev/null and b/src/Brutario.Win/Resources/rotate-left-solid.png differ
diff --git a/src/Brutario.Win/Resources/rotate-right-solid.png b/src/Brutario.Win/Resources/rotate-right-solid.png
new file mode 100644
index 0000000..9c4c081
Binary files /dev/null and b/src/Brutario.Win/Resources/rotate-right-solid.png differ
diff --git a/src/Brutario.Win/Resources/scissors-solid.png b/src/Brutario.Win/Resources/scissors-solid.png
new file mode 100644
index 0000000..3416474
Binary files /dev/null and b/src/Brutario.Win/Resources/scissors-solid.png differ
diff --git a/src/Brutario.Win/Resources/trash-solid.png b/src/Brutario.Win/Resources/trash-solid.png
new file mode 100644
index 0000000..4b285a7
Binary files /dev/null and b/src/Brutario.Win/Resources/trash-solid.png differ
diff --git a/src/Brutario.Win/SpecialThanksForm.Designer.cs b/src/Brutario.Win/SpecialThanksForm.Designer.cs
new file mode 100644
index 0000000..d6d39db
--- /dev/null
+++ b/src/Brutario.Win/SpecialThanksForm.Designer.cs
@@ -0,0 +1,91 @@
+//
+// Copyright (c) 2022 spel werdz rite. All rights reserved. Licensed
+// under GNU Affero General Public License. See LICENSE in project
+// root for full license information, or visit
+// https://www.gnu.org/licenses/#AGPL
+//
+
+namespace Brutario.Win
+{
+ partial class SpecialThanksForm
+ {
+ ///
+ /// Required designer variable.
+ ///
+ private System.ComponentModel.IContainer components = null;
+
+ ///
+ /// Clean up any resources being used.
+ ///
+ /// true if managed resources should be disposed; otherwise, false.
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing && (components != null))
+ {
+ components.Dispose();
+ }
+ base.Dispose(disposing);
+ }
+
+ #region Windows Form Designer generated code
+
+ ///
+ /// Required method for Designer support - do not modify
+ /// the contents of this method with the code editor.
+ ///
+ private void InitializeComponent()
+ {
+ rtbCredits = new RichTextBox();
+ btnOK = new Button();
+ SuspendLayout();
+ //
+ // rtbCredits
+ //
+ rtbCredits.Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right;
+ rtbCredits.Location = new Point(0, 0);
+ rtbCredits.Margin = new Padding(4, 5, 4, 5);
+ rtbCredits.Name = "rtbCredits";
+ rtbCredits.ReadOnly = true;
+ rtbCredits.Size = new Size(871, 1129);
+ rtbCredits.TabIndex = 0;
+ rtbCredits.Text = "";
+ rtbCredits.WordWrap = false;
+ //
+ // btnOK
+ //
+ btnOK.Anchor = AnchorStyles.Bottom | AnchorStyles.Right;
+ btnOK.DialogResult = DialogResult.OK;
+ btnOK.Location = new Point(756, 1140);
+ btnOK.Margin = new Padding(4, 5, 4, 5);
+ btnOK.Name = "btnOK";
+ btnOK.Size = new Size(100, 35);
+ btnOK.TabIndex = 1;
+ btnOK.Text = "&OK";
+ btnOK.UseVisualStyleBackColor = true;
+ //
+ // SpecialThanksForm
+ //
+ AcceptButton = btnOK;
+ AutoScaleDimensions = new SizeF(8F, 20F);
+ AutoScaleMode = AutoScaleMode.Font;
+ ClientSize = new Size(872, 1194);
+ Controls.Add(btnOK);
+ Controls.Add(rtbCredits);
+ Margin = new Padding(4, 5, 4, 5);
+ MinimizeBox = false;
+ MinimumSize = new Size(887, 184);
+ Name = "SpecialThanksForm";
+ ShowIcon = false;
+ ShowInTaskbar = false;
+ StartPosition = FormStartPosition.CenterScreen;
+ Text = "Credits";
+ Load += SpecialThanksForm_Load;
+ ResumeLayout(false);
+ }
+
+ #endregion
+
+ private System.Windows.Forms.RichTextBox rtbCredits;
+ private System.Windows.Forms.Button btnOK;
+ }
+}
diff --git a/src/Brutario.Win/SpecialThanksForm.cs b/src/Brutario.Win/SpecialThanksForm.cs
new file mode 100644
index 0000000..5af285b
--- /dev/null
+++ b/src/Brutario.Win/SpecialThanksForm.cs
@@ -0,0 +1,30 @@
+//
+// Copyright (c) 2022 spel werdz rite. All rights reserved. Licensed under GNU Affero
+// General Public License. See LICENSE in project root for full license
+// information, or visit https://www.gnu.org/licenses/#AGPL
+//
+
+namespace Brutario.Win;
+
+using System.Windows.Forms;
+
+public partial class SpecialThanksForm : Form
+{
+ public SpecialThanksForm()
+ {
+ InitializeComponent();
+ }
+
+ private void SpecialThanksForm_Load(object sender, EventArgs e)
+ {
+ var dir = Path.GetDirectoryName(Application.ExecutablePath);
+ var path = Path.Combine(dir!, "Credits.rtf");
+ if (!File.Exists(path))
+ {
+ rtbCredits.Text = "Oops the Credits file is missing";
+ return;
+ }
+
+ rtbCredits.LoadFile(path);
+ }
+}
diff --git a/src/Brutario.Win/SpecialThanksForm.resx b/src/Brutario.Win/SpecialThanksForm.resx
new file mode 100644
index 0000000..8b2ff64
--- /dev/null
+++ b/src/Brutario.Win/SpecialThanksForm.resx
@@ -0,0 +1,120 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
\ No newline at end of file
diff --git a/src/Brutario.Win/Views/EditorDialogBase.Designer.cs b/src/Brutario.Win/Views/EditorDialogBase.Designer.cs
new file mode 100644
index 0000000..88d6af7
--- /dev/null
+++ b/src/Brutario.Win/Views/EditorDialogBase.Designer.cs
@@ -0,0 +1,43 @@
+//
+// Copyright (c) 2022 spel werdz rite. All rights reserved. Licensed
+// under GNU Affero General Public License. See LICENSE in project
+// root for full license information, or visit
+// https://www.gnu.org/licenses/#AGPL
+//
+
+namespace Brutario.Win.Views
+{
+ partial class EditorDialogBase
+ {
+ ///
+ /// Required designer variable.
+ ///
+ private System.ComponentModel.IContainer components = null;
+
+ ///
+ /// Clean up any resources being used.
+ ///
+ /// true if managed resources should be disposed; otherwise, false.
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing && (components != null))
+ {
+ components.Dispose();
+ }
+ base.Dispose(disposing);
+ }
+
+ #region Component Designer generated code
+
+ ///
+ /// Required method for Designer support - do not modify
+ /// the contents of this method with the code editor.
+ ///
+ private void InitializeComponent()
+ {
+ components = new System.ComponentModel.Container();
+ }
+
+ #endregion
+ }
+}
diff --git a/src/Brutario.Win/Views/EditorDialogBase.cs b/src/Brutario.Win/Views/EditorDialogBase.cs
new file mode 100644
index 0000000..ccac965
--- /dev/null
+++ b/src/Brutario.Win/Views/EditorDialogBase.cs
@@ -0,0 +1,54 @@
+//
+// Copyright (c) 2022 spel werdz rite. All rights reserved. Licensed under GNU Affero
+// General Public License. See LICENSE in project root for full license
+// information, or visit https://www.gnu.org/licenses/#AGPL
+//
+
+namespace Brutario.Win.Views;
+
+using System;
+using System.ComponentModel;
+using System.Windows.Forms;
+
+public partial class EditorDialogBase : Component
+{
+ private IWin32Window? win32Window;
+
+ public EditorDialogBase()
+ {
+ InitializeComponent();
+ }
+
+ public EditorDialogBase(IContainer container)
+ {
+ container.Add(this);
+
+ InitializeComponent();
+ }
+
+ public event EventHandler? OwnerChanged;
+
+ public IWin32Window? Owner
+ {
+ get
+ {
+ return win32Window;
+ }
+
+ set
+ {
+ if (Owner == value)
+ {
+ return;
+ }
+
+ win32Window = value;
+ OnOwnerChanged(EventArgs.Empty);
+ }
+ }
+
+ protected virtual void OnOwnerChanged(EventArgs e)
+ {
+ OwnerChanged?.Invoke(this, e);
+ }
+}
diff --git a/src/Brutario.Win/Views/EditorDialogBase.resx b/src/Brutario.Win/Views/EditorDialogBase.resx
new file mode 100644
index 0000000..1af7de1
--- /dev/null
+++ b/src/Brutario.Win/Views/EditorDialogBase.resx
@@ -0,0 +1,120 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
\ No newline at end of file
diff --git a/src/Brutario.Win/Views/ExceptionView.Designer.cs b/src/Brutario.Win/Views/ExceptionView.Designer.cs
new file mode 100644
index 0000000..1c2ba1c
--- /dev/null
+++ b/src/Brutario.Win/Views/ExceptionView.Designer.cs
@@ -0,0 +1,36 @@
+namespace Brutario.Win.Views
+{
+ partial class ExceptionView
+ {
+ ///
+ /// Required designer variable.
+ ///
+ private System.ComponentModel.IContainer components = null;
+
+ ///
+ /// Clean up any resources being used.
+ ///
+ /// true if managed resources should be disposed; otherwise, false.
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing && (components != null))
+ {
+ components.Dispose();
+ }
+ base.Dispose(disposing);
+ }
+
+ #region Component Designer generated code
+
+ ///
+ /// Required method for Designer support - do not modify
+ /// the contents of this method with the code editor.
+ ///
+ private void InitializeComponent()
+ {
+ components = new System.ComponentModel.Container();
+ }
+
+ #endregion
+ }
+}
diff --git a/src/Brutario.Win/Views/ExceptionView.cs b/src/Brutario.Win/Views/ExceptionView.cs
new file mode 100644
index 0000000..0bc221e
--- /dev/null
+++ b/src/Brutario.Win/Views/ExceptionView.cs
@@ -0,0 +1,82 @@
+namespace Brutario.Win.Views;
+
+using System;
+using System.ComponentModel;
+
+using Controls;
+
+using Core;
+
+public partial class ExceptionView : EditorDialogBase, IExceptionView
+{
+ private string? _title;
+
+ public ExceptionView()
+ : base()
+ {
+ InitializeComponent();
+ }
+
+ public ExceptionView(IContainer container)
+ : base(container)
+ {
+ InitializeComponent();
+ }
+
+ public event EventHandler? TitleChanged;
+
+ public string? Title
+ {
+ get
+ {
+ return _title;
+ }
+
+ set
+ {
+ if (Title == value)
+ {
+ return;
+ }
+
+ _title = value;
+ OnTitleChanged(EventArgs.Empty);
+ }
+ }
+
+ public void Show(Exception ex)
+ {
+ Show(ex.Message);
+ }
+
+ public void Show(string? message)
+ {
+ _ = RtlAwareMessageBox.Show(
+ Owner,
+ message,
+ Title,
+ MessageBoxButtons.OK,
+ MessageBoxIcon.Warning);
+ }
+
+ public bool ShowAndPromptRetry(Exception ex)
+ {
+ return ShowAndPromptRetry(ex.Message);
+ }
+
+ public bool ShowAndPromptRetry(string? message)
+ {
+ var dialogResult = RtlAwareMessageBox.Show(
+ Owner,
+ message,
+ Title,
+ MessageBoxButtons.RetryCancel,
+ MessageBoxIcon.Warning);
+ return dialogResult == DialogResult.Retry;
+ }
+
+ protected virtual void OnTitleChanged(EventArgs e)
+ {
+ TitleChanged?.Invoke(this, e);
+ }
+}
diff --git a/src/Brutario.Win/Views/FileNameSelectorBase.cs b/src/Brutario.Win/Views/FileNameSelectorBase.cs
new file mode 100644
index 0000000..59dfa60
--- /dev/null
+++ b/src/Brutario.Win/Views/FileNameSelectorBase.cs
@@ -0,0 +1,68 @@
+namespace Brutario.Win.Views;
+
+using System;
+using System.ComponentModel;
+
+using Core;
+
+public abstract class FileNameSelectorBase : EditorDialogBase, IFileNameSelector
+{
+ public FileNameSelectorBase()
+ {
+ }
+
+ public FileNameSelectorBase(IContainer container)
+ {
+ container.Add(this);
+ }
+
+ public event EventHandler? FileNameChanged;
+
+ public string? FileName
+ {
+ get
+ {
+ return FileDialog.FileName;
+ }
+
+ set
+ {
+ if (FileName == value)
+ {
+ return;
+ }
+
+ FileDialog.FileName = value;
+ OnFileNameChanged(EventArgs.Empty);
+ }
+ }
+
+ protected abstract FileDialog FileDialog
+ {
+ get;
+ }
+
+ public PromptResult Prompt()
+ {
+ return FileDialog.ShowDialog() switch
+ {
+ DialogResult.OK => PromptResult.Yes,
+ _ => PromptResult.No,
+ };
+ }
+
+ protected virtual void OnFileNameChanged(EventArgs e)
+ {
+ FileNameChanged?.Invoke(this, e);
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ FileDialog.Dispose();
+ }
+
+ base.Dispose(disposing);
+ }
+}
diff --git a/src/Brutario.Win/Views/HeaderEditor.Designer.cs b/src/Brutario.Win/Views/HeaderEditor.Designer.cs
new file mode 100644
index 0000000..3f09ddd
--- /dev/null
+++ b/src/Brutario.Win/Views/HeaderEditor.Designer.cs
@@ -0,0 +1,53 @@
+//
+// Copyright (c) 2022 spel werdz rite. All rights reserved. Licensed
+// under GNU Affero General Public License. See LICENSE in project
+// root for full license information, or visit
+// https://www.gnu.org/licenses/#AGPL
+//
+
+namespace Brutario.Win.Views
+{
+ partial class HeaderEditor
+ {
+ ///
+ /// Required designer variable.
+ ///
+ private System.ComponentModel.IContainer components = null;
+
+ ///
+ /// Clean up any resources being used.
+ ///
+ /// true if managed resources should be disposed; otherwise, false.
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing && (components != null))
+ {
+ components.Dispose();
+ }
+ base.Dispose(disposing);
+ }
+
+ #region Component Designer generated code
+
+ ///
+ /// Required method for Designer support - do not modify
+ /// the contents of this method with the code editor.
+ ///
+ private void InitializeComponent()
+ {
+ this.components = new System.ComponentModel.Container();
+ this.headerEditorDialog = new Brutario.Win.Dialogs.HeaderEditorDialog(this.components);
+ //
+ // headerEditorDialog
+ //
+ this.headerEditorDialog.ShowHelp = false;
+ this.headerEditorDialog.Title = "Edit Header";
+ this.headerEditorDialog.AreaHeaderChanged += new System.EventHandler(this.HeaderEditorDialog_AreaHeaderChanged);
+
+ }
+
+ #endregion
+
+ private Dialogs.HeaderEditorDialog headerEditorDialog;
+ }
+}
diff --git a/src/Brutario.Win/Views/HeaderEditor.cs b/src/Brutario.Win/Views/HeaderEditor.cs
new file mode 100644
index 0000000..500425f
--- /dev/null
+++ b/src/Brutario.Win/Views/HeaderEditor.cs
@@ -0,0 +1,59 @@
+//
+// Copyright (c) 2022 spel werdz rite. All rights reserved. Licensed under GNU Affero
+// General Public License. See LICENSE in project root for full license
+// information, or visit https://www.gnu.org/licenses/#AGPL
+//
+
+namespace Brutario.Win.Views;
+
+using System;
+using System.ComponentModel;
+using System.Windows.Forms;
+
+using Core;
+
+using Maseya.Smas.Smb1.AreaData.HeaderData;
+
+using static System.ComponentModel.DesignerSerializationVisibility;
+
+public sealed partial class HeaderEditor : EditorDialogBase, IHeaderEditorView
+{
+ public HeaderEditor()
+ : base()
+ {
+ InitializeComponent();
+ }
+
+ public HeaderEditor(IContainer container)
+ : base(container)
+ {
+ InitializeComponent();
+ }
+
+ public event EventHandler? AreaHeaderChanged;
+
+ [Browsable(false)]
+ [DesignerSerializationVisibility(Hidden)]
+ public AreaHeader AreaHeader
+ {
+ get
+ {
+ return headerEditorDialog.AreaHeader;
+ }
+
+ set
+ {
+ headerEditorDialog.AreaHeader = value;
+ }
+ }
+
+ public bool Prompt()
+ {
+ return headerEditorDialog.ShowDialog(Owner) == DialogResult.OK;
+ }
+
+ private void HeaderEditorDialog_AreaHeaderChanged(object? sender, EventArgs e)
+ {
+ AreaHeaderChanged?.Invoke(this, EventArgs.Empty);
+ }
+}
diff --git a/src/Brutario.Win/Views/HeaderEditor.resx b/src/Brutario.Win/Views/HeaderEditor.resx
new file mode 100644
index 0000000..53c0997
--- /dev/null
+++ b/src/Brutario.Win/Views/HeaderEditor.resx
@@ -0,0 +1,126 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ 17, 17
+
+
+ False
+
+
\ No newline at end of file
diff --git a/src/Brutario.Win/Views/ObjectEditor.Designer.cs b/src/Brutario.Win/Views/ObjectEditor.Designer.cs
new file mode 100644
index 0000000..7a0bdd5
--- /dev/null
+++ b/src/Brutario.Win/Views/ObjectEditor.Designer.cs
@@ -0,0 +1,54 @@
+//
+// Copyright (c) 2022 spel werdz rite. All rights reserved. Licensed
+// under GNU Affero General Public License. See LICENSE in project
+// root for full license information, or visit
+// https://www.gnu.org/licenses/#AGPL
+//
+
+namespace Brutario.Win.Views
+{
+ partial class ObjectEditor
+ {
+ ///
+ /// Required designer variable.
+ ///
+ private System.ComponentModel.IContainer components = null;
+
+ ///
+ /// Clean up any resources being used.
+ ///
+ /// true if managed resources should be disposed; otherwise, false.
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing && (components != null))
+ {
+ components.Dispose();
+ }
+ base.Dispose(disposing);
+ }
+
+ #region Component Designer generated code
+
+ ///
+ /// Required method for Designer support - do not modify
+ /// the contents of this method with the code editor.
+ ///
+ private void InitializeComponent()
+ {
+ this.components = new System.ComponentModel.Container();
+ this.objectEditorDialog = new Brutario.Win.Dialogs.ObjectEditorDialog(this.components);
+ //
+ // objectEditorDialog
+ //
+ this.objectEditorDialog.ShowHelp = false;
+ this.objectEditorDialog.Title = "Object Editor";
+ this.objectEditorDialog.AreaPlatformTypeChanged += new System.EventHandler(this.ObjectEditorDialog_AreaPlatformTypeChanged);
+ this.objectEditorDialog.AreaObjectCommandChanged += new System.EventHandler(this.ObjectEditorDialog_AreaObjectCommandChanged);
+
+ }
+
+ #endregion
+
+ private Dialogs.ObjectEditorDialog objectEditorDialog;
+ }
+}
diff --git a/src/Brutario.Win/Views/ObjectEditor.cs b/src/Brutario.Win/Views/ObjectEditor.cs
new file mode 100644
index 0000000..dde67a8
--- /dev/null
+++ b/src/Brutario.Win/Views/ObjectEditor.cs
@@ -0,0 +1,85 @@
+//
+// Copyright (c) 2022 spel werdz rite. All rights reserved. Licensed under GNU Affero
+// General Public License. See LICENSE in project root for full license
+// information, or visit https://www.gnu.org/licenses/#AGPL
+//
+
+namespace Brutario.Win.Views;
+
+using System;
+using System.ComponentModel;
+using System.Windows.Forms;
+
+using Core;
+
+using Maseya.Smas.Smb1.AreaData.ObjectData;
+
+using static System.ComponentModel.DesignerSerializationVisibility;
+
+public sealed partial class ObjectEditor : EditorDialogBase, IObjectEditorView
+{
+ public ObjectEditor()
+ : base()
+ {
+ InitializeComponent();
+ }
+
+ public ObjectEditor(IContainer container)
+ : base(container)
+ {
+ InitializeComponent();
+ }
+
+ public event EventHandler? AreaPlatformTypeChanged;
+
+ public event EventHandler? AreaObjectCommandChanged;
+
+ [Browsable(false)]
+ [DesignerSerializationVisibility(Hidden)]
+ public AreaPlatformType AreaPlatformType
+ {
+ get
+ {
+ return objectEditorDialog.AreaPlatformType;
+ }
+
+ set
+ {
+ objectEditorDialog.AreaPlatformType = value;
+ }
+ }
+
+ [Browsable(false)]
+ [DesignerSerializationVisibility(Hidden)]
+ public UIAreaObjectCommand AreaObjectCommand
+ {
+ get
+ {
+ return objectEditorDialog.AreaObjectCommand;
+ }
+
+ set
+ {
+ objectEditorDialog.AreaObjectCommand = value;
+ }
+ }
+
+ public bool PromptConfirm()
+ {
+ return objectEditorDialog.ShowDialog(Owner) == DialogResult.OK;
+ }
+
+ private void ObjectEditorDialog_AreaPlatformTypeChanged(
+ object? sender,
+ EventArgs e)
+ {
+ AreaPlatformTypeChanged?.Invoke(this, EventArgs.Empty);
+ }
+
+ private void ObjectEditorDialog_AreaObjectCommandChanged(
+ object? sender,
+ EventArgs e)
+ {
+ AreaObjectCommandChanged?.Invoke(this, EventArgs.Empty);
+ }
+}
diff --git a/src/Brutario.Win/Views/ObjectEditor.resx b/src/Brutario.Win/Views/ObjectEditor.resx
new file mode 100644
index 0000000..a135619
--- /dev/null
+++ b/src/Brutario.Win/Views/ObjectEditor.resx
@@ -0,0 +1,126 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ 17, 17
+
+
+ False
+
+
\ No newline at end of file
diff --git a/src/Brutario.Win/Views/ObjectListView.Designer.cs b/src/Brutario.Win/Views/ObjectListView.Designer.cs
new file mode 100644
index 0000000..9e0e7fd
--- /dev/null
+++ b/src/Brutario.Win/Views/ObjectListView.Designer.cs
@@ -0,0 +1,52 @@
+namespace Brutario.Win.Views;
+
+partial class ObjectListView
+{
+ ///
+ /// Required designer variable.
+ ///
+ private System.ComponentModel.IContainer components = null;
+
+ ///
+ /// Clean up any resources being used.
+ ///
+ /// true if managed resources should be disposed; otherwise, false.
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing && (components != null))
+ {
+ components.Dispose();
+ }
+ base.Dispose(disposing);
+ }
+
+ #region Component Designer generated code
+
+ ///
+ /// Required method for Designer support - do not modify
+ /// the contents of this method with the code editor.
+ ///
+ private void InitializeComponent()
+ {
+ components = new System.ComponentModel.Container();
+ objectListDialog = new Dialogs.ObjectListDialog(components);
+ //
+ // objectListDialog
+ //
+ objectListDialog.Owner = null;
+ objectListDialog.ShowHelp = false;
+ objectListDialog.Title = "Object List";
+ objectListDialog.SelectedIndexChanged += List_SelectedIndexChanged;
+ objectListDialog.EditItem += Dialog_EditItem;
+ objectListDialog.AddItem_Click += Add_Click;
+ objectListDialog.DeleteItem_Click += Delete_Click;
+ objectListDialog.ClearItems_Click += Clear_Click;
+ objectListDialog.MoveItemDown_Click += MovieDown_Click;
+ objectListDialog.MoveItemUp_Click += MoveUp_Click;
+ objectListDialog.VisibleChanged += Dialog_VisibleChanged;
+ }
+
+ #endregion
+
+ private Dialogs.ObjectListDialog objectListDialog;
+}
diff --git a/src/Brutario.Win/Views/ObjectListView.cs b/src/Brutario.Win/Views/ObjectListView.cs
new file mode 100644
index 0000000..5b42d72
--- /dev/null
+++ b/src/Brutario.Win/Views/ObjectListView.cs
@@ -0,0 +1,182 @@
+namespace Brutario.Win.Views;
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+
+using Brutario.Core;
+
+using Core.Views;
+
+using Maseya.Smas.Smb1.AreaData.ObjectData;
+
+using static System.ComponentModel.DesignerSerializationVisibility;
+using static Dialogs.BaseForms.ObjectListForm;
+
+public sealed partial class ObjectListView : Component, IObjectListView
+{
+ public ObjectListView()
+ {
+ InitializeComponent();
+ InitializeComponent2();
+ }
+
+ public ObjectListView(IContainer container)
+ {
+ container.Add(this);
+
+ InitializeComponent();
+ InitializeComponent2();
+ }
+
+ public event EventHandler? AreaPlatformTypeChanged;
+
+ public event EventHandler? SelectedIndexChanged;
+
+ public event EventHandler? EditItem;
+
+ public event EventHandler? AddItem_Click;
+
+ public event EventHandler? DeleteItem_Click;
+
+ public event EventHandler? ClearItems_Click;
+
+ public event EventHandler? MoveItemDown_Click;
+
+ public event EventHandler? MoveItemUp_Click;
+
+ public event EventHandler? VisibleChanged;
+
+ [Browsable(false)]
+ [DesignerSerializationVisibility(Hidden)]
+ public int SelectedIndex
+ {
+ get
+ {
+ return objectListDialog.SelectedIndex;
+ }
+
+ set
+ {
+ objectListDialog.SelectedIndex = value;
+ }
+ }
+
+ [Browsable(false)]
+ [DesignerSerializationVisibility(Hidden)]
+ public AreaPlatformType AreaPlatformType
+ {
+ get
+ {
+ return objectListDialog.AreaPlatformType;
+ }
+
+ set
+ {
+ objectListDialog.AreaPlatformType = value;
+ }
+ }
+
+ [Browsable(false)]
+ [DesignerSerializationVisibility(Hidden)]
+ public ItemCollection Items
+ {
+ get
+ {
+ return objectListDialog.Items;
+ }
+ }
+
+ IList IObjectListView.Items
+ {
+ get
+ {
+ return Items;
+ }
+ }
+
+ [Browsable(false)]
+ [DesignerSerializationVisibility(Hidden)]
+ public bool Visible
+ {
+ get
+ {
+ return objectListDialog.Visible;
+ }
+
+ set
+ {
+ objectListDialog.Visible = value;
+ }
+ }
+
+ [Browsable(false)]
+ [DesignerSerializationVisibility(Hidden)]
+ public Form Owner
+ {
+ get
+ {
+ return objectListDialog.Owner;
+ }
+
+ set
+ {
+ objectListDialog.Owner = value;
+ }
+ }
+
+ private void InitializeComponent2()
+ {
+ objectListDialog.AreaPlatformTypeChanged += ObjectListDialog_AreaPlatformTypeChanged;
+ objectListDialog.SelectedIndexChanged += ObjectListDialog_SelectedIndexChanged;
+ }
+
+ private void ObjectListDialog_AreaPlatformTypeChanged(object? sender, EventArgs e)
+ {
+ AreaPlatformTypeChanged?.Invoke(this, EventArgs.Empty);
+ }
+
+ private void ObjectListDialog_SelectedIndexChanged(object? sender, EventArgs e)
+ {
+ SelectedIndexChanged?.Invoke(this, EventArgs.Empty);
+ }
+
+ private void Add_Click(object? sender, EventArgs e)
+ {
+ AddItem_Click?.Invoke(this, EventArgs.Empty);
+ }
+
+ private void Delete_Click(object? sender, EventArgs e)
+ {
+ DeleteItem_Click?.Invoke(this, EventArgs.Empty);
+ }
+
+ private void Clear_Click(object? sender, EventArgs e)
+ {
+ ClearItems_Click?.Invoke(this, EventArgs.Empty);
+ }
+
+ private void MovieDown_Click(object? sender, EventArgs e)
+ {
+ MoveItemDown_Click?.Invoke(this, EventArgs.Empty);
+ }
+
+ private void MoveUp_Click(object? sender, EventArgs e)
+ {
+ MoveItemUp_Click?.Invoke(this, EventArgs.Empty);
+ }
+
+ private void List_SelectedIndexChanged(object sender, EventArgs e)
+ {
+ SelectedIndexChanged?.Invoke(this, EventArgs.Empty);
+ }
+
+ private void Dialog_EditItem(object sender, EventArgs e)
+ {
+ EditItem?.Invoke(this, EventArgs.Empty);
+ }
+
+ private void Dialog_VisibleChanged(object sender, EventArgs e)
+ {
+ VisibleChanged?.Invoke(this, EventArgs.Empty);
+ }
+}
diff --git a/src/Brutario.Win/Views/ObjectListView.resx b/src/Brutario.Win/Views/ObjectListView.resx
new file mode 100644
index 0000000..b54d94b
--- /dev/null
+++ b/src/Brutario.Win/Views/ObjectListView.resx
@@ -0,0 +1,126 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ 33, 33
+
+
+ False
+
+
\ No newline at end of file
diff --git a/src/Brutario.Win/Views/OpenFileNameSelector.cs b/src/Brutario.Win/Views/OpenFileNameSelector.cs
new file mode 100644
index 0000000..768ec3c
--- /dev/null
+++ b/src/Brutario.Win/Views/OpenFileNameSelector.cs
@@ -0,0 +1,29 @@
+namespace Brutario.Win.Views;
+
+using System.ComponentModel;
+using System.Windows.Forms;
+
+public class OpenFileNameSelector : FileNameSelectorBase
+{
+ public OpenFileNameSelector()
+ : base()
+ {
+ OpenFileDialog = new OpenFileDialog();
+ }
+
+ public OpenFileNameSelector(IContainer container)
+ : base(container)
+ {
+ OpenFileDialog = new OpenFileDialog();
+ }
+
+ public OpenFileDialog OpenFileDialog { get; }
+
+ protected override FileDialog FileDialog
+ {
+ get
+ {
+ return OpenFileDialog;
+ }
+ }
+}
diff --git a/src/Brutario.Win/Views/SaveFileNameSelector.cs b/src/Brutario.Win/Views/SaveFileNameSelector.cs
new file mode 100644
index 0000000..fafddec
--- /dev/null
+++ b/src/Brutario.Win/Views/SaveFileNameSelector.cs
@@ -0,0 +1,28 @@
+namespace Brutario.Win.Views;
+
+using System.ComponentModel;
+using System.Windows.Forms;
+
+public class SaveFileNameSelector : FileNameSelectorBase
+{
+ public SaveFileNameSelector() : base()
+ {
+ SaveFileDialog = new SaveFileDialog();
+ }
+
+ public SaveFileNameSelector(IContainer container)
+ : base(container)
+ {
+ SaveFileDialog = new SaveFileDialog();
+ }
+
+ public SaveFileDialog SaveFileDialog { get; }
+
+ protected override FileDialog FileDialog
+ {
+ get
+ {
+ return SaveFileDialog;
+ }
+ }
+}
diff --git a/src/Brutario.Win/Views/SaveOnClosePrompt.Designer.cs b/src/Brutario.Win/Views/SaveOnClosePrompt.Designer.cs
new file mode 100644
index 0000000..5ad7143
--- /dev/null
+++ b/src/Brutario.Win/Views/SaveOnClosePrompt.Designer.cs
@@ -0,0 +1,36 @@
+namespace Brutario.Win.Views
+{
+ partial class SaveOnClosePrompt
+ {
+ ///
+ /// Required designer variable.
+ ///
+ private System.ComponentModel.IContainer components = null;
+
+ ///
+ /// Clean up any resources being used.
+ ///
+ /// true if managed resources should be disposed; otherwise, false.
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing && (components != null))
+ {
+ components.Dispose();
+ }
+ base.Dispose(disposing);
+ }
+
+ #region Component Designer generated code
+
+ ///
+ /// Required method for Designer support - do not modify
+ /// the contents of this method with the code editor.
+ ///
+ private void InitializeComponent()
+ {
+ components = new System.ComponentModel.Container();
+ }
+
+ #endregion
+ }
+}
diff --git a/src/Brutario.Win/Views/SaveOnClosePrompt.cs b/src/Brutario.Win/Views/SaveOnClosePrompt.cs
new file mode 100644
index 0000000..8e0af96
--- /dev/null
+++ b/src/Brutario.Win/Views/SaveOnClosePrompt.cs
@@ -0,0 +1,95 @@
+namespace Brutario.Win.Views;
+
+using System;
+using System.ComponentModel;
+using System.Windows.Forms;
+
+using Controls;
+
+using Core;
+
+public partial class SaveOnClosePrompt : EditorDialogBase, ISaveOnClosePrompt
+{
+ private string? _text;
+
+ private string? _caption;
+
+ public SaveOnClosePrompt()
+ : base()
+ {
+ InitializeComponent();
+ }
+
+ public SaveOnClosePrompt(IContainer container)
+ : base(container)
+ {
+ InitializeComponent();
+ }
+
+ public event EventHandler? TextChanged;
+
+ public event EventHandler? CaptionChanged;
+
+ public string? Caption
+ {
+ get
+ {
+ return _caption;
+ }
+
+ set
+ {
+ if (Caption == value)
+ {
+ return;
+ }
+
+ _caption = value;
+ OnCaptionChanged(EventArgs.Empty);
+ }
+ }
+
+ public string? Text
+ {
+ get
+ {
+ return _text;
+ }
+
+ set
+ {
+ if (Text == value)
+ {
+ return;
+ }
+
+ _text = value;
+ OnTextChanged(EventArgs.Empty);
+ }
+ }
+
+ public PromptResult Prompt()
+ {
+ return RtlAwareMessageBox.Show(
+ Owner,
+ Text,
+ Caption,
+ MessageBoxButtons.YesNoCancel) switch
+ {
+ DialogResult.Yes => PromptResult.Yes,
+ DialogResult.No => PromptResult.No,
+ DialogResult.Cancel => PromptResult.Cancel,
+ _ => throw new InvalidOperationException(),
+ };
+ }
+
+ protected virtual void OnCaptionChanged(EventArgs e)
+ {
+ CaptionChanged?.Invoke(this, e);
+ }
+
+ protected virtual void OnTextChanged(EventArgs e)
+ {
+ TextChanged?.Invoke(this, e);
+ }
+}
diff --git a/src/Brutario.Win/Views/SpriteEditor.Designer.cs b/src/Brutario.Win/Views/SpriteEditor.Designer.cs
new file mode 100644
index 0000000..a35c994
--- /dev/null
+++ b/src/Brutario.Win/Views/SpriteEditor.Designer.cs
@@ -0,0 +1,53 @@
+//
+// Copyright (c) 2022 spel werdz rite. All rights reserved. Licensed
+// under GNU Affero General Public License. See LICENSE in project
+// root for full license information, or visit
+// https://www.gnu.org/licenses/#AGPL
+//
+
+namespace Brutario.Win.Views
+{
+ partial class SpriteEditor
+ {
+ ///
+ /// Required designer variable.
+ ///
+ private System.ComponentModel.IContainer components = null;
+
+ ///
+ /// Clean up any resources being used.
+ ///
+ /// true if managed resources should be disposed; otherwise, false.
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing && (components != null))
+ {
+ components.Dispose();
+ }
+ base.Dispose(disposing);
+ }
+
+ #region Component Designer generated code
+
+ ///
+ /// Required method for Designer support - do not modify
+ /// the contents of this method with the code editor.
+ ///
+ private void InitializeComponent()
+ {
+ this.components = new System.ComponentModel.Container();
+ this.spriteEditorDialog = new Brutario.Win.Dialogs.SpriteEditorDialog(this.components);
+ //
+ // spriteEditorDialog
+ //
+ this.spriteEditorDialog.ShowHelp = false;
+ this.spriteEditorDialog.Title = "Sprite Editor";
+ this.spriteEditorDialog.AreaSpriteCommandChanged += new System.EventHandler(this.SpriteEditorDialog_AreaSpriteCommandChanged);
+
+ }
+
+ #endregion
+
+ private Dialogs.SpriteEditorDialog spriteEditorDialog;
+ }
+}
diff --git a/src/Brutario.Win/Views/SpriteEditor.cs b/src/Brutario.Win/Views/SpriteEditor.cs
new file mode 100644
index 0000000..100a644
--- /dev/null
+++ b/src/Brutario.Win/Views/SpriteEditor.cs
@@ -0,0 +1,59 @@
+//
+// Copyright (c) 2022 spel werdz rite. All rights reserved. Licensed under GNU Affero
+// General Public License. See LICENSE in project root for full license
+// information, or visit https://www.gnu.org/licenses/#AGPL
+//
+
+namespace Brutario.Win.Views;
+
+using System;
+using System.ComponentModel;
+using System.Windows.Forms;
+
+using Core;
+
+using static System.ComponentModel.DesignerSerializationVisibility;
+
+public sealed partial class SpriteEditor : EditorDialogBase, ISpriteEditorView
+{
+ public SpriteEditor()
+ : base()
+ {
+ InitializeComponent();
+ }
+
+ public SpriteEditor(IContainer container)
+ : base(container)
+ {
+ InitializeComponent();
+ }
+
+ public event EventHandler? AreaSpriteCommandChanged;
+
+ [Browsable(false)]
+ [DesignerSerializationVisibility(Hidden)]
+ public UIAreaSpriteCommand AreaSpriteCommand
+ {
+ get
+ {
+ return spriteEditorDialog.AreaSpriteCommand;
+ }
+
+ set
+ {
+ spriteEditorDialog.AreaSpriteCommand = value;
+ }
+ }
+
+ public bool PromptConfirm()
+ {
+ return spriteEditorDialog.ShowDialog(Owner) == DialogResult.OK;
+ }
+
+ private void SpriteEditorDialog_AreaSpriteCommandChanged(
+ object? sender,
+ EventArgs e)
+ {
+ AreaSpriteCommandChanged?.Invoke(this, EventArgs.Empty);
+ }
+}
diff --git a/src/Brutario.Win/Views/SpriteEditor.resx b/src/Brutario.Win/Views/SpriteEditor.resx
new file mode 100644
index 0000000..21fe587
--- /dev/null
+++ b/src/Brutario.Win/Views/SpriteEditor.resx
@@ -0,0 +1,126 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ 17, 17
+
+
+ False
+
+
\ No newline at end of file
diff --git a/src/RedBlackTree/Color.cs b/src/RedBlackTree/Color.cs
new file mode 100644
index 0000000..e540dac
--- /dev/null
+++ b/src/RedBlackTree/Color.cs
@@ -0,0 +1,7 @@
+namespace Maseya;
+
+internal enum Color
+{
+ Red,
+ Black,
+}
diff --git a/src/RedBlackTree/Direction.cs b/src/RedBlackTree/Direction.cs
new file mode 100644
index 0000000..fd792bb
--- /dev/null
+++ b/src/RedBlackTree/Direction.cs
@@ -0,0 +1,7 @@
+namespace Maseya;
+
+internal enum Direction
+{
+ Left = 0,
+ Right = 1,
+}
diff --git a/src/RedBlackTree/ExtensionMethods.cs b/src/RedBlackTree/ExtensionMethods.cs
new file mode 100644
index 0000000..29b6c67
--- /dev/null
+++ b/src/RedBlackTree/ExtensionMethods.cs
@@ -0,0 +1,69 @@
+namespace Maseya;
+using System.Collections.Generic;
+using System.Linq;
+
+public static class ExtensionMethods
+{
+ public static RedBlackTreeNode Min(
+ this RedBlackTreeNode node)
+ {
+ var result = node;
+ while (result.Left != null)
+ {
+ result = result.Left;
+ }
+
+ return result;
+ }
+
+ public static RedBlackTreeNode Max(
+ this RedBlackTreeNode node)
+ {
+ var result = node;
+ while (result.Right != null)
+ {
+ result = result.Right;
+ }
+
+ return result;
+ }
+
+ public static IEnumerable>
+ InOrderTraversal(this RedBlackTreeNode? node)
+ {
+ var nodes = Enumerable.Empty>();
+ if (node is not null)
+ {
+ nodes = nodes.Concat(InOrderTraversal(node.Left));
+ nodes = nodes.Concat(Enumerable.Repeat(node, 1));
+ nodes = nodes.Concat(InOrderTraversal(node.Right));
+ }
+
+ return nodes;
+ }
+
+ public static IEnumerable>
+ ForwardTraversal(this RedBlackTreeNode? node)
+ {
+ while (node is not null)
+ {
+ yield return node;
+ node = node.Next;
+ }
+ }
+
+ public static bool IsBlack(this RedBlackTreeNode? node)
+ {
+ return node == null || node.Color == Color.Black;
+ }
+
+ public static bool IsRed(this RedBlackTreeNode? node)
+ {
+ return !node.IsBlack();
+ }
+
+ internal static Direction Reverse(this Direction direction)
+ {
+ return (Direction)(1 - (int)direction);
+ }
+}
diff --git a/src/RedBlackTree/RedBlackTree.cs b/src/RedBlackTree/RedBlackTree.cs
new file mode 100644
index 0000000..c432e84
--- /dev/null
+++ b/src/RedBlackTree/RedBlackTree.cs
@@ -0,0 +1,797 @@
+namespace Maseya;
+
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+
+public sealed class RedBlackTree :
+ IDictionary
+{
+ public RedBlackTree(IComparer? comparer = null)
+ {
+ Comparer = comparer ?? Comparer.Default;
+ Keys = new KeyCollection(this);
+ Values = new ValueCollection(this);
+ }
+
+ public RedBlackTreeNode? Root { get; private set; }
+
+ public bool IsEmpty { get { return Root is null; } }
+
+ public int Count { get; private set; }
+
+ public IComparer Comparer { get; }
+
+ public KeyCollection Keys { get; }
+
+ public ValueCollection Values { get; }
+
+ ICollection IDictionary.Keys { get { return Keys; } }
+
+ ICollection IDictionary.Values { get { return Values; } }
+
+ bool ICollection>.IsReadOnly { get { return false; } }
+
+ public TValue this[TKey key]
+ {
+ get
+ {
+ return TryGetValue(key, out var value)
+ ? value
+ : throw new KeyNotFoundException();
+ }
+
+ set
+ {
+ var node = FindKey(key);
+ if (node is null)
+ {
+ Add(key, value);
+ }
+ else
+ {
+ node.Value = value;
+ }
+ }
+ }
+
+ private RedBlackTreeNode? this[RedBlackTreeNode node]
+ {
+ set
+ {
+ if (value is not null)
+ {
+ value.Parent = node.Parent;
+ value.Color = Color.Black;
+ }
+
+ if (node.Parent is null)
+ {
+ Root = value;
+ }
+ else
+ {
+ var parent = node.Parent;
+ var direction = GetDirection(node);
+ parent[direction] = value;
+ }
+ }
+ }
+
+ public bool ContainsKey(TKey key)
+ {
+ return FindKey(key) is not null;
+ }
+
+ public bool ContainsValue(TValue value, IEqualityComparer? comparer = null)
+ {
+ return FindValue(value, comparer) is not null;
+ }
+
+ public bool Contains(
+ TKey key,
+ TValue value,
+ IEqualityComparer? comparer = null)
+ {
+ return Find(key, value, comparer) is not null;
+ }
+
+ public bool TryGetValue(TKey key, [MaybeNullWhen(false)] out TValue value)
+ {
+ var node = FindKey(key);
+ if (node is null)
+ {
+ value = default;
+ return false;
+ }
+
+ value = node.Value;
+ return true;
+ }
+
+ public RedBlackTreeNode? FindKey(TKey key)
+ {
+ var node = Root;
+ while (node is not null)
+ {
+ var comparison = Comparer.Compare(key, node.Key);
+ if (comparison == 0)
+ {
+ break;
+ }
+
+ node = comparison < 0 ? node.Left : node.Right;
+ }
+
+ return node;
+ }
+
+ public RedBlackTreeNode? LowerBound(TKey key)
+ {
+ var node = Root;
+ RedBlackTreeNode? result = null;
+ while (node is not null)
+ {
+ var comparison = Comparer.Compare(key, node.Key);
+ if (comparison <= 0)
+ {
+ result = node;
+ node = node.Left;
+ }
+ else
+ {
+ node = node.Right;
+ }
+ }
+
+ return result;
+ }
+
+ public RedBlackTreeNode? UpperBound(TKey key)
+ {
+ var node = Root;
+ RedBlackTreeNode? result = null;
+ while (node is not null)
+ {
+ var comparison = Comparer.Compare(key, node.Key);
+ if (comparison < 0)
+ {
+ result = node;
+ node = node.Left;
+ }
+ else
+ {
+ node = node.Right;
+ }
+ }
+
+ return result;
+ }
+
+ public RedBlackTreeNode? FindValue(
+ TValue value,
+ IEqualityComparer? comparer)
+ {
+ comparer ??= EqualityComparer.Default;
+ return Root
+ .InOrderTraversal()
+ .FirstOrDefault(x => comparer.Equals(x.Value, value));
+ }
+
+ public RedBlackTreeNode? Find(
+ TKey key,
+ TValue value,
+ IEqualityComparer? comparer = null)
+ {
+ var first = LowerBound(key);
+ if (first == null)
+ {
+ return null;
+ }
+
+ comparer ??= EqualityComparer.Default;
+ return first
+ .ForwardTraversal()
+ .TakeWhile(x => Comparer.Compare(x.Key, key) == 0)
+ .FirstOrDefault(x => comparer.Equals(x.Value, value));
+ }
+
+ public void Add(TKey key, TValue value)
+ {
+ Count++;
+ if (Root is null)
+ {
+ // When setting root node, it is guaranteed to be the only node in the
+ // tree. So we do not need to balance it or set any connecting nodes.
+ Root = new RedBlackTreeNode(key, value);
+ return;
+ }
+
+ var parent = Root;
+ Direction direction;
+ {
+ _loop:
+ direction = Comparer.Compare(key, parent.Key) < 0
+ ? Direction.Left
+ : Direction.Right;
+ var next = parent[direction];
+ if (next is not null)
+ {
+ parent = next;
+ goto _loop;
+ }
+ }
+
+ var node = new RedBlackTreeNode(key, value, parent, direction);
+ BalanceInsertedNode(node);
+ }
+
+ public bool Remove(TKey key)
+ {
+ return Remove(FindKey(key));
+ }
+
+ public bool Remove(
+ TKey key,
+ TValue value,
+ IEqualityComparer? comparer = null)
+ {
+ return Remove(Find(key, value, comparer));
+ }
+
+ public void Clear()
+ {
+ Root = null;
+ Count = 0;
+ }
+
+ public KeyValuePair Min()
+ {
+ return Root is not null
+ ? Root.Min().KeyValuePair
+ : throw new InvalidOperationException("Tree contains no elements.");
+ }
+
+ public KeyValuePair Max()
+ {
+ return Root is not null
+ ? Root.Max().KeyValuePair
+ : throw new InvalidOperationException("Tree contains no elements.");
+ }
+
+ public IEnumerator> GetEnumerator()
+ {
+ return Root.InOrderTraversal().Select(x => x.KeyValuePair).GetEnumerator();
+ }
+
+ bool ICollection>.Contains(
+ KeyValuePair item)
+ {
+ return Contains(item.Key, item.Value);
+ }
+
+ void ICollection>.Add(KeyValuePair item)
+ {
+ Add(item.Key, item.Value);
+ }
+
+ bool ICollection>.Remove(KeyValuePair item)
+ {
+ return Remove(item.Key, item.Value);
+ }
+
+ void ICollection>.CopyTo(
+ KeyValuePair[] array,
+ int arrayIndex)
+ {
+ if ((uint)arrayIndex >= (uint)array.Length)
+ {
+ throw new ArgumentOutOfRangeException(nameof(arrayIndex));
+ }
+
+ if (arrayIndex + Count > array.Length)
+ {
+ throw new ArgumentOutOfRangeException(nameof(arrayIndex));
+ }
+
+ foreach (var kvp in this)
+ {
+ array[arrayIndex++] = kvp;
+ }
+ }
+
+ System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
+ {
+ return GetEnumerator();
+ }
+
+ private static Direction GetDirection(RedBlackTreeNode node)
+ {
+ return node == node.Parent!.Left
+ ? Direction.Left
+ : Direction.Right;
+ }
+
+ private static RedBlackTreeNode LeftRotate(
+ RedBlackTreeNode node)
+ {
+ var child = node.Right!;
+
+ node.Right = child.Left;
+ if (node.Right is not null)
+ {
+ node.Right.Parent = node;
+ }
+
+ child.Left = node;
+ child.Parent = node.Parent;
+ node.Parent = child;
+ if (child.Parent is not null)
+ {
+ var direction = node == child.Parent.Left
+ ? Direction.Left
+ : Direction.Right;
+ child.Parent[direction] = child;
+ }
+
+ return child;
+ }
+
+ private static RedBlackTreeNode RightRotate(
+ RedBlackTreeNode node)
+ {
+ var child = node.Left!;
+
+ node.Left = child.Right;
+ if (node.Left is not null)
+ {
+ node.Left.Parent = node;
+ }
+
+ child.Right = node;
+ child.Parent = node.Parent;
+ node.Parent = child;
+ if (child.Parent is not null)
+ {
+ var direction = node == child.Parent.Left
+ ? Direction.Left
+ : Direction.Right;
+ child.Parent[direction] = child;
+ }
+
+ return child;
+ }
+
+ private bool Remove(RedBlackTreeNode? node)
+ {
+ if (node is null)
+ {
+ return false;
+ }
+
+ Count--;
+
+ // If the node has two children, we swap the node with its successor, which is
+ // guaranteed to either be a leaf or have only a right child. If it had a left
+ // child, it would contradict this node actually be the successor. This breaks
+ // the well-ordering of the collection only temporarily until the active node
+ // is removed.
+ if (node.Left is not null && node.Right is not null)
+ {
+ (node.Next!.Key, node.Key) = (node.Key, node.Next.Key);
+ (node.Next.Value, node.Value) = (node.Value, node.Next.Value);
+ node = node.Next;
+ }
+
+ if (node.Prev != null)
+ {
+ node.Prev.Next = node.Next;
+ }
+
+ if (node.Next != null)
+ {
+ node.Next.Prev = node.Prev;
+ }
+
+ // From now on, the node has either zero or one children.
+
+ // If the node has exactly one child, then the node must be black and the child
+ // must be red. Otherwise, there would be a black violation along the null
+ // child path. In such a case, we simply remove the active node and replace it
+ // with the child. We must repaint the child node black to guarantee no red or
+ // black violations occur, ensuring the tree remains balanced.
+ if (node.Left is not null)
+ {
+ this[node] = node.Left;
+ }
+ else if (node.Right is not null)
+ {
+ this[node] = node.Right;
+ }
+ else if (node == Root)
+ {
+ // If the node is the root and has no children, then it can simply be removed.
+ this[node] = null;
+ }
+ else
+ {
+ var parent = node.Parent!;
+ var direction = GetDirection(node);
+ this[node] = null;
+
+ // Finally, we end up at the non-trivial case of a black leaf node.
+ if (node.IsBlack())
+ {
+ BalanceDeletedNode(parent, direction);
+ }
+ }
+
+ return true;
+ }
+
+ private RedBlackTreeNode Rotate(
+ RedBlackTreeNode node,
+ Direction direction)
+ {
+ var result = direction == Direction.Left
+ ? LeftRotate(node)
+ : RightRotate(node);
+
+ // Check if we need to update the root node after the rotation.
+ if (result.Parent is null)
+ {
+ Root = result;
+ }
+
+ return result;
+ }
+
+ private void BalanceInsertedNode(RedBlackTreeNode node)
+ {
+ // Loop invariants:
+ // * The current node is red at the start of each iteration
+ // * Requirement 3 (no parent-child relationship has two red nodes) is satisfied
+ // everywhere except for possibly the current node and its parent.
+ // * All other RB tree properties are satisfied everywhere.
+ {
+ _loop:
+ var parent = node.Parent;
+
+ // Case 1: The parent node is black. Case 3: The active node is the root.
+ // These cases satisfy requirement 3, making the whole tree RB-balanced.
+ if (parent.IsBlack())
+ {
+ return;
+ }
+
+ // Moving forward, the parent node is guaranteed to be red.
+ var grandparent = parent!.Parent;
+
+ // Case 4: The parent is red and is the root node.
+ if (grandparent is null)
+ {
+ // The node and its parent are both red, causing a red-violation.
+ // However, the parent is also the root, so we can change its color to
+ // black, fixing the violation.
+ parent.Color = Color.Black;
+ return;
+ }
+
+ // Moving forward, parent is red and has a non-null grandparent.
+ var uncle = parent == grandparent.Left
+ ? grandparent.Right
+ : grandparent.Left;
+
+ // Case 2: The parent and uncle nodes are both red
+ if (uncle.IsRed())
+ {
+ // We recolor the parent black to fix the red-violation between the
+ // node and its parent.
+ parent.Color = Color.Black;
+
+ // However, now there's a black a violation from the grandparent to the
+ // current node since we introduced a new black node, so we recolor the
+ // grandparent node to red.
+ grandparent.Color = Color.Red;
+
+ // But now there is a black-violation from the grandparent to the
+ // uncle, and a red-violation since they're both red now, so we color
+ // the uncle black, which fixes both violations.
+ uncle!.Color = Color.Black;
+
+ // This makes the subtree balanced, but the grandparent may be in
+ // violation now if its parent is also red. We set it as the active
+ // node and restart the loop.
+ node = grandparent;
+ goto _loop;
+ }
+
+ // From now on, the uncle is black.
+
+ // Case 5: The node is an inner grandchild.
+ var direction = GetDirection(parent);
+ if (direction != GetDirection(node))
+ {
+ // After rotating, we fall through to case 6.
+ parent = Rotate(parent, direction);
+ }
+
+ // Case 6: The node is now an outer grandchild. Since the parent is red and
+ // the tree was balanced up to now before inserting, the grandparent must
+ // be black.
+
+ // If we change the parent color to black, then the red violation between
+ // active node and parent goes away.
+ parent.Color = Color.Black;
+
+ // However, now a black violation occurs from grandparent to active node as
+ // we introduced a new black node. So we set the grandparent to red.
+ grandparent.Color = Color.Red;
+
+ // This does not add any red violations from grandparent to uncle since
+ // uncle is black, but this does cause a black-violation since the path
+ // from grandparent to uncle lost a black node. We can balance this with a
+ // rotation on the grandparent.
+ _ = Rotate(grandparent, direction.Reverse());
+
+ // This makes the tree full balanced, so we are done.
+ return;
+ }
+ }
+
+ private void BalanceDeletedNode(
+ RedBlackTreeNode parent,
+ Direction direction)
+ {
+ // The first iteration now syncs with the regular loop, which we express as a
+ // goto loop. The loop has the following invariants: The black height of the
+ // active node is one less than the other nodes, meaning there is always a
+ // black violation. All other balancing requirements are satisfied.
+ {
+ _loop:
+ // During any iteration, the parent node may be red or black. However, the
+ // sibling may never be null as that would imply a black violation existed
+ // before the (black) active node was removed.
+ var sibling = parent[direction.Reverse()]!;
+
+ // We get the nephews, which can possibly be null.
+ var distantNephew = sibling[direction.Reverse()];
+ var closeNephew = sibling[direction];
+
+ // Case 3: The sibling is red. This implies that the sibling its parent and
+ // children must all be black.
+ if (sibling.IsRed())
+ {
+ // The goal is to transform this into either case 4, 5, or 6, which all
+ // require the sibling node be black.
+ sibling.Color = Color.Black;
+
+ // However, this introduces a black violation along the sibling
+ // subtree, so we must change the parent color to red.
+ parent.Color = Color.Red;
+
+ // But if the grandparent is also red, then we create a red violation
+ // outside of our parent subtree. We can fix this issue by rotating the
+ // parent node.
+ _ = Rotate(parent, direction);
+ goto _loop;
+ }
+
+ // From now on, the sibling node is black.
+
+ // Case 4: The parent is red. This implies that both nephews must be black.
+ if (parent.IsRed() && distantNephew.IsBlack() && closeNephew.IsBlack())
+ {
+ // If we swap the colors of the parent and sibling nodes, this doesn't
+ // affect the black height of any paths going through the sibling
+ // subtree, nor does it introduce any red violations. It does, however,
+ // add one to the black height toward the delete node, which balances
+ // the tree.
+ parent.Color = Color.Black;
+ sibling.Color = Color.Red;
+
+ // Because the tree is balanced, we are done.
+ return;
+ }
+
+ // From now on, the parent is black.
+
+ if (distantNephew.IsBlack())
+ {
+ if (closeNephew.IsBlack())
+ {
+ // Case 1: Parent, sibling, and nephews are all black. If we
+ // repaint the sibling node to red, this balances the parent
+ // subtree after removal. But now the whole parent subtree is in
+ // black violation with the rest of the tree (if parent is not the root).
+ sibling.Color = Color.Red;
+
+ // Case 2: The parent is the root.
+ if (parent.Parent is null)
+ {
+ // This implies that one black node has been removed from every
+ // path, decreasing the total black height of the tree by 1,
+ // and removing the black violation of the tree.
+ return;
+ }
+
+ direction = GetDirection(parent);
+ parent = parent.Parent!;
+
+ // Otherwise, we go to the next iteration.
+ goto _loop;
+ }
+
+ // Case 5: The sibling node is black and the close nephew is red and
+ // the distant nephew is black. Our goal is to transform this into case
+ // 6, where the distant nephew is red.
+
+ closeNephew!.Color = Color.Black;
+
+ sibling.Color = Color.Red;
+
+ // Finally, we do the actual rotation.
+ distantNephew = sibling;
+ sibling = Rotate(sibling, direction.Reverse());
+ }
+
+ // Case 6: The sibling node is black and the distant nephew is red. We do
+ // not care about the color of the close nephew.
+
+ // If we swap the colors of the sibling and parent nodes, they do preserve
+ // the black heights of all paths through the sibling subtree, introducing
+ // no black violations. Plus, making the parent node black will not
+ // introduce any red violations either.
+ sibling.Color = parent.Color;
+ parent.Color = Color.Black;
+
+ // If we rotate the parent node, then the sibling becomes the new parent,
+ // increasing the black height to the deleted node by 1, which fixes its
+ // black violation.
+ _ = Rotate(parent, direction);
+
+ // However, the distant nephew subtree lost its black parent, introducing a
+ // black violation here. But we can fix this by making the node black.
+ distantNephew!.Color = Color.Black;
+
+ // This balances the entire tree, and we are done.
+ return;
+ }
+ }
+
+ public sealed class KeyCollection : ICollection, IReadOnlyCollection
+ {
+ internal KeyCollection(RedBlackTree tree)
+ {
+ Tree = tree;
+ }
+
+ public int Count
+ {
+ get
+ {
+ return Tree.Count;
+ }
+ }
+
+ bool ICollection.IsReadOnly { get { return true; } }
+
+ private RedBlackTree Tree { get; }
+
+ public bool Contains(TKey value)
+ {
+ return Tree.ContainsKey(value);
+ }
+
+ public void CopyTo(TKey[] array, int arrayIndex)
+ {
+ if ((uint)arrayIndex >= (uint)array.Length)
+ {
+ throw new ArgumentOutOfRangeException(nameof(arrayIndex));
+ }
+
+ if (arrayIndex + Count > array.Length)
+ {
+ throw new ArgumentOutOfRangeException(nameof(arrayIndex));
+ }
+
+ foreach (var key in this)
+ {
+ array[arrayIndex++] = key;
+ }
+ }
+
+ public IEnumerator GetEnumerator()
+ {
+ return Tree.Select(x => x.Key).GetEnumerator();
+ }
+
+ System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
+ {
+ return GetEnumerator();
+ }
+
+ void ICollection.Add(TKey item)
+ {
+ throw new NotSupportedException();
+ }
+
+ void ICollection.Clear()
+ {
+ throw new NotSupportedException();
+ }
+
+ bool ICollection.Remove(TKey item)
+ {
+ throw new NotSupportedException();
+ }
+ }
+
+ public sealed class ValueCollection : ICollection
+ {
+ internal ValueCollection(RedBlackTree tree)
+ {
+ Tree = tree;
+ }
+
+ public int Count
+ {
+ get
+ {
+ return Tree.Count;
+ }
+ }
+
+ bool ICollection.IsReadOnly { get { return true; } }
+
+ private RedBlackTree Tree { get; }
+
+ public bool Contains(TValue value)
+ {
+ return Tree.ContainsValue(value);
+ }
+
+ public void CopyTo(TValue[] array, int arrayIndex)
+ {
+ if ((uint)arrayIndex >= (uint)array.Length)
+ {
+ throw new ArgumentOutOfRangeException(nameof(arrayIndex));
+ }
+
+ if (arrayIndex + Count > array.Length)
+ {
+ throw new ArgumentOutOfRangeException(nameof(arrayIndex));
+ }
+
+ foreach (var value in this)
+ {
+ array[arrayIndex++] = value;
+ }
+ }
+
+ public IEnumerator GetEnumerator()
+ {
+ return Tree.Select(x => x.Value).GetEnumerator();
+ }
+
+ System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
+ {
+ return GetEnumerator();
+ }
+
+ void ICollection.Add(TValue item)
+ {
+ throw new NotSupportedException();
+ }
+
+ void ICollection.Clear()
+ {
+ throw new NotSupportedException();
+ }
+
+ bool ICollection.Remove(TValue item)
+ {
+ throw new NotSupportedException();
+ }
+ }
+}
diff --git a/src/RedBlackTree/RedBlackTree.csproj b/src/RedBlackTree/RedBlackTree.csproj
new file mode 100644
index 0000000..b07a21c
--- /dev/null
+++ b/src/RedBlackTree/RedBlackTree.csproj
@@ -0,0 +1,13 @@
+
+
+
+ net6.0
+ enable
+ enable
+ spel werdz rite
+ Maseya
+ en-US
+ Maseya
+
+
+
diff --git a/src/RedBlackTree/RedBlackTreeNode.cs b/src/RedBlackTree/RedBlackTreeNode.cs
new file mode 100644
index 0000000..c4a38b0
--- /dev/null
+++ b/src/RedBlackTree/RedBlackTreeNode.cs
@@ -0,0 +1,105 @@
+namespace Maseya;
+using System.Collections.Generic;
+
+public class RedBlackTreeNode
+{
+ internal RedBlackTreeNode(TKey key, TValue value)
+ {
+ Key = key;
+ Value = value;
+ }
+
+ internal RedBlackTreeNode(
+ TKey key,
+ TValue value,
+ RedBlackTreeNode parent,
+ Direction direction)
+ : this(key, value)
+ {
+ Parent = parent;
+ Color = Color.Red;
+ if (parent != null)
+ {
+ parent[direction] = this;
+ if (direction == Direction.Left)
+ {
+ Prev = parent.Prev;
+ if (Prev != null)
+ {
+ Prev.Next = this;
+ }
+
+ Next = parent;
+ parent.Prev = this;
+ }
+ else
+ {
+ Next = parent.Next;
+ if (Next != null)
+ {
+ Next.Prev = this;
+ }
+
+ Prev = parent;
+ parent.Next = this;
+ }
+ }
+ }
+
+ public TKey Key { get; internal set; }
+
+ public TValue Value { get; set; }
+
+ public RedBlackTreeNode? Left { get; internal set; }
+
+ public RedBlackTreeNode? Right { get; internal set; }
+
+ public RedBlackTreeNode? Parent { get; internal set; }
+
+ public RedBlackTreeNode? Prev { get; internal set; }
+
+ public RedBlackTreeNode? Next { get; internal set; }
+
+ public KeyValuePair KeyValuePair
+ {
+ get
+ {
+ return new KeyValuePair(Key, Value);
+ }
+
+ internal set
+ {
+ Key = value.Key;
+ Value = value.Value;
+ }
+ }
+
+ internal Color Color { get; set; }
+
+ internal RedBlackTreeNode? this[Direction direction]
+ {
+ get
+ {
+ return direction == Direction.Left
+ ? Left
+ : Right;
+ }
+
+ set
+ {
+ if (direction == Direction.Left)
+ {
+ Left = value;
+ }
+ else
+ {
+ Right = value;
+ }
+ }
+ }
+
+ public override string ToString()
+ {
+ return $"{{{Key}, {Value}}}";
+ }
+}
diff --git a/src/Smas/Smas.csproj b/src/Smas/Smas.csproj
new file mode 100644
index 0000000..cbaac45
--- /dev/null
+++ b/src/Smas/Smas.csproj
@@ -0,0 +1,31 @@
+
+
+
+ net8.0
+ enable
+ enable
+ True
+ Maseya.$(MSBuildProjectName.Replace(" ", "_"))
+ en-US
+
+
+
+
+
+
+
+
+ True
+ True
+ Resources.resx
+
+
+
+
+
+ ResXFileCodeGenerator
+ Resources.Designer.cs
+
+
+
+
diff --git a/src/Smas/Smb1/AreaData/AreaLoader.cs b/src/Smas/Smb1/AreaData/AreaLoader.cs
new file mode 100644
index 0000000..870bb82
--- /dev/null
+++ b/src/Smas/Smb1/AreaData/AreaLoader.cs
@@ -0,0 +1,467 @@
+//
+// Copyright (c) 2022 spel werdz rite. All rights reserved. Licensed under GNU Affero
+// General Public License. See LICENSE in project root for full license
+// information, or visit https://www.gnu.org/licenses/#AGPL
+//
+
+namespace Maseya.Smas.Smb1.AreaData;
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+using HeaderData;
+
+using ObjectData;
+
+using Snes;
+
+using SpriteData;
+
+public class AreaLoader
+{
+ public AreaLoader(Rom rom, AreaLoaderPointers pointers)
+ {
+ NumberOfWorlds = rom.ReadByte(pointers.NumberOfWorldsAddress);
+ NumberOfAreas = pointers.NumberOfAreas;
+
+ WorldAreaNumberOffsetTable = rom.ReadBytesIndirect(
+ pointers.WorldAreaNumberOffsetPointer,
+ NumberOfWorlds);
+
+ if (WorldAreaNumberOffsetTable.Any(
+ AreaNumber => AreaNumber >= pointers.AreaNumberTableSize))
+ {
+ throw new ArgumentException(
+ "World area number table exceeds max number of areas.");
+ }
+
+ AreaNumberTable = rom.ReadBytesIndirect(
+ pointers.AreaNumberTablePointer,
+ pointers.AreaNumberTableSize);
+
+ ObjectAreaIndexTable = rom.ReadBytesIndirect(
+ pointers.ObjectAreaTypeOffsetPointer,
+ count: 4);
+
+ // These indices require the index table be built first.
+ var objectAreaIndices = AreaNumberTable.Select(
+ areaNumber => GetObjectAreaIndex(areaNumber));
+ if (objectAreaIndices.Any(areaIndex => areaIndex >= NumberOfAreas))
+ {
+ throw new ArgumentException("Area data is invalid!");
+ }
+
+ SpriteAreaIndexTable = rom.ReadBytesIndirect(
+ pointers.SpriteAreaTypeOffsetPointer,
+ count: 4);
+
+ var spriteAreaIndices = AreaNumberTable.Select(
+ areaNumber => GetSpriteAreaIndex(areaNumber));
+ if (spriteAreaIndices.Any(areaIndex => areaIndex >= NumberOfAreas))
+ {
+ throw new ArgumentException("Area data is invalid!");
+ }
+
+ var objectBank = pointers.ObjectLowBytePointer & 0xFF0000;
+ var objectLows = rom.ReadBytesIndirect(
+ pointers.ObjectLowBytePointer,
+ NumberOfAreas);
+ var objectHighs = rom.ReadBytesIndirect(
+ pointers.ObjectHighBytePointer,
+ NumberOfAreas);
+
+ var spriteBank = pointers.SpriteLowBytePointer & 0xFF0000;
+ var spriteLows = rom.ReadBytesIndirect(
+ pointers.SpriteLowBytePointer,
+ NumberOfAreas);
+ var spriteHighs = rom.ReadBytesIndirect(
+ pointers.SpriteHighBytePointer,
+ NumberOfAreas);
+
+ Headers = new AreaHeader[NumberOfAreas];
+ AreaObjectData = new AreaObjectCommand[NumberOfAreas][];
+ AreaSpriteData = new AreaSpriteCommand[NumberOfAreas][];
+ for (var i = 0; i < NumberOfAreas; i++)
+ {
+ var objectAddress = objectBank | (objectHighs[i] << 8) | objectLows[i];
+ Headers[i] = rom.ReadInt16(objectAddress);
+ AreaObjectData[i] = GetAreaObjectData(
+ rom.EnumerateBytes(objectAddress + 2, 0x10000))
+ .ToArray();
+
+ var spriteAddress = spriteBank | (spriteHighs[i] << 8) | spriteLows[i];
+ AreaSpriteData[i] = GetAreaSpriteData(rom.EnumerateBytes(spriteAddress, 0x10000))
+ .ToArray();
+ }
+
+ SortedObjectAreaTypes =
+ Enumerable.Range(0, 4).Select(i => (AreaType)i).ToArray();
+ Array.Sort(
+ SortedObjectAreaTypes,
+ (x, y) => ObjectAreaIndexTable[(int)x] - ObjectAreaIndexTable[(int)y]);
+
+ SortedSpriteAreaTypes =
+ Enumerable.Range(0, 4).Select(i => (AreaType)i).ToArray();
+ Array.Sort(
+ SortedSpriteAreaTypes,
+ (x, y) => SpriteAreaIndexTable[(int)x] - SpriteAreaIndexTable[(int)y]);
+
+ // The counts should be the same when using sprite or object data. We choose
+ // object data.
+ AreaObjectCounts = new int[4];
+ for (var i = 1; i < 4; i++)
+ {
+ AreaObjectCounts[i - 1] =
+ ObjectAreaIndexTable[(int)SortedObjectAreaTypes[i]] -
+ ObjectAreaIndexTable[(int)SortedObjectAreaTypes[i - 1]];
+ }
+
+ AreaObjectCounts[3] = NumberOfAreas
+ - ObjectAreaIndexTable[(int)SortedObjectAreaTypes[3]];
+ }
+
+ public int NumberOfAreas
+ {
+ get;
+ }
+
+ public int NumberOfWorlds
+ {
+ get;
+ }
+
+ public AreaHeader[] Headers
+ {
+ get;
+ }
+
+ public AreaObjectCommand[][] AreaObjectData
+ {
+ get;
+ }
+
+ public AreaSpriteCommand[][] AreaSpriteData
+ {
+ get;
+ }
+
+ private byte[] WorldAreaNumberOffsetTable
+ {
+ get;
+ }
+
+ private byte[] AreaNumberTable
+ {
+ get;
+ }
+
+ private byte[] ObjectAreaIndexTable
+ {
+ get;
+ }
+
+ private AreaType[] SortedObjectAreaTypes
+ {
+ get;
+ }
+
+ private byte[] SpriteAreaIndexTable
+ {
+ get;
+ }
+
+ private AreaType[] SortedSpriteAreaTypes
+ {
+ get;
+ }
+
+ private int[] AreaObjectCounts
+ {
+ get;
+ }
+
+ public static AreaType GetAreaType(int areaNumber)
+ {
+ return (AreaType)((areaNumber >> 5) & 3);
+ }
+
+ ///
+ /// Returns the area number without the bits.
+ ///
+ public static int GetReducedAreaNumber(int areaNumber)
+ {
+ return areaNumber & 0x1F;
+ }
+
+ public static int AreaNumberFromAreaType(AreaType areaType, int reducedAreaNumber)
+ {
+ return reducedAreaNumber | ((int)areaType << 5);
+ }
+
+ public bool IsValidAreaNumberForSpriteData(int areaNumber)
+ {
+ var index = GetSpriteAreaIndex(areaNumber);
+ return (uint)index < (uint)NumberOfAreas;
+ }
+
+ public bool IsValidAreaNumberForObjectData(int areaNumber)
+ {
+ var index = GetObjectAreaIndex(areaNumber);
+ return (uint)index < (uint)NumberOfAreas;
+ }
+
+ public byte GetAreaNumber(int world, int level)
+ {
+ var worldAreaStart = WorldAreaNumberOffsetTable[world];
+ var levelIndex = worldAreaStart + level;
+ return (byte)(levelIndex < NumberOfAreas
+ ? AreaNumberTable[levelIndex] & 0x7F
+ : 0);
+ }
+
+ public int GetObjectAreaIndex(int areaNumber)
+ {
+ var areaType = GetAreaType(areaNumber);
+ var reducedAreaNumber = GetReducedAreaNumber(areaNumber);
+ var areaTypeIndex = ObjectAreaIndexTable[(int)areaType];
+
+ return reducedAreaNumber + areaTypeIndex;
+ }
+
+ public int GetSpriteAreaIndex(int areaNumber)
+ {
+ var areaType = GetAreaType(areaNumber);
+ var reducedAreaNumber = GetReducedAreaNumber(areaNumber);
+ var areaTypeIndex = SpriteAreaIndexTable[(int)areaType];
+
+ return reducedAreaNumber + areaTypeIndex;
+ }
+
+ public int AreaNumberFromObjectAreaIndex(int objectAreaIndex)
+ {
+ var areaType = AreaTypeFromObjectAreaIndex(objectAreaIndex);
+ var reducedAreaNumber = objectAreaIndex - ObjectAreaIndexTable[(int)areaType];
+ return AreaNumberFromAreaType(areaType, reducedAreaNumber);
+ }
+
+ public AreaType AreaTypeFromObjectAreaIndex(int objectAreaIndex)
+ {
+ for (var i = SortedObjectAreaTypes.Length; --i >= 0;)
+ {
+ if (objectAreaIndex >= ObjectAreaIndexTable[(int)SortedObjectAreaTypes[i]])
+ {
+ return SortedObjectAreaTypes[i];
+ }
+ }
+
+ throw new ArgumentOutOfRangeException(
+ nameof(objectAreaIndex),
+ "Value does not correspond to valid areaType location.");
+ }
+
+ public int AreaNumberFromSpriteAreaIndex(int spriteAreaIndex)
+ {
+ var areaType = AreaTypeFromSpriteAreaIndex(spriteAreaIndex);
+ var reducedAreaNumber = spriteAreaIndex - SpriteAreaIndexTable[(int)areaType];
+ return AreaNumberFromAreaType(areaType, reducedAreaNumber);
+ }
+
+ public AreaType AreaTypeFromSpriteAreaIndex(int spriteAreaIndex)
+ {
+ for (var i = SortedSpriteAreaTypes.Length; --i >= 0;)
+ {
+ if (spriteAreaIndex >= SpriteAreaIndexTable[(int)SortedSpriteAreaTypes[i]])
+ {
+ return SortedSpriteAreaTypes[i];
+ }
+ }
+
+ throw new ArgumentOutOfRangeException(
+ nameof(spriteAreaIndex),
+ "Value does not correspond to valid areaType location.");
+ }
+
+ public void WriteObjectData(
+ int index,
+ AreaHeader areaHeader,
+ IEnumerable objectData)
+ {
+ Headers[index] = areaHeader;
+ AreaObjectData[index] = objectData.ToArray();
+ }
+
+ public void WriteSpriteData(
+ int index,
+ IEnumerable spriteData)
+ {
+ AreaSpriteData[index] = spriteData.ToArray();
+ }
+
+ public void WriteToGameData(Rom rom, AreaLoaderPointers pointers)
+ {
+ var totalObjectSize = 0;
+ var objectData = new byte[NumberOfAreas][];
+ var objectOffsets = new int[NumberOfAreas];
+
+ var totalSpriteSize = 0;
+ var spriteData = new byte[NumberOfAreas][];
+ var spriteOffsets = new int[NumberOfAreas];
+
+ for (var i = 0; i < NumberOfAreas; i++)
+ {
+ var spriteAreaIndex = i;
+ var areaNumber = AreaNumberFromSpriteAreaIndex(spriteAreaIndex);
+ var objectAreaIndex = GetObjectAreaIndex(areaNumber);
+ var data = AreaObjectData[objectAreaIndex].ToBytes().ToArray();
+ objectData[objectAreaIndex] = new byte[2 + data.Length];
+ objectData[objectAreaIndex][0] = Headers[objectAreaIndex].Value1;
+ objectData[objectAreaIndex][1] = Headers[objectAreaIndex].Value2;
+ data.CopyTo(objectData[objectAreaIndex], 2);
+
+ objectOffsets[objectAreaIndex] = totalObjectSize;
+ totalObjectSize += objectData[objectAreaIndex].Length;
+
+ spriteData[spriteAreaIndex] =
+ AreaSpriteData[spriteAreaIndex].ToBytes().ToArray();
+ if (spriteData[spriteAreaIndex].Length > 1 || spriteAreaIndex != 0x0F)
+ {
+ spriteOffsets[spriteAreaIndex] = totalSpriteSize;
+ totalSpriteSize += spriteData[spriteAreaIndex].Length;
+ }
+ else
+ {
+ spriteOffsets[spriteAreaIndex] = totalSpriteSize - 1;
+ }
+ }
+
+ var maxSize = pointers.AreaDataEndPointer - pointers.AreaDataStartPointer;
+ if (totalObjectSize + totalSpriteSize > maxSize)
+ {
+ throw new InvalidOperationException();
+ }
+
+ var objectLow = new byte[NumberOfAreas];
+ var objectHigh = new byte[NumberOfAreas];
+ var spriteLow = new byte[NumberOfAreas];
+ var spriteHigh = new byte[NumberOfAreas];
+ for (var i = 0; i < NumberOfAreas; i++)
+ {
+ var spriteAddress = spriteOffsets[i] + pointers.AreaDataStartPointer;
+ spriteLow[i] = (byte)spriteAddress;
+ spriteHigh[i] = (byte)(spriteAddress >> 8);
+ rom.WriteBytes(spriteAddress, spriteData[i]);
+
+ var objectAddress = objectOffsets[i] + totalSpriteSize
+ + pointers.AreaDataStartPointer;
+
+ objectLow[i] = (byte)objectAddress;
+ objectHigh[i] = (byte)(objectAddress >> 8);
+ rom.WriteBytes(objectAddress, objectData[i]);
+ }
+
+ rom.WriteBytesIndirect(pointers.SpriteHighBytePointer, spriteHigh);
+ rom.WriteBytesIndirect(pointers.SpriteLowBytePointer, spriteLow);
+ rom.WriteBytesIndirect(pointers.ObjectHighBytePointer, objectHigh);
+ rom.WriteBytesIndirect(pointers.ObjectLowBytePointer, objectLow);
+
+ rom.WriteBytesIndirect(
+ pointers.SpriteAreaTypeOffsetPointer,
+ SpriteAreaIndexTable);
+ rom.WriteBytesIndirect(
+ pointers.ObjectAreaTypeOffsetPointer,
+ ObjectAreaIndexTable);
+ rom.WriteBytesIndirect(
+ pointers.AreaNumberTablePointer,
+ AreaNumberTable);
+ rom.WriteBytesIndirect(
+ pointers.WorldAreaNumberOffsetPointer,
+ WorldAreaNumberOffsetTable);
+
+ rom.WriteByte(pointers.NumberOfWorldsAddress, NumberOfWorlds);
+ }
+
+ private static IEnumerable GetAreaObjectData(
+ IEnumerable bytes)
+ {
+ using var en = bytes.GetEnumerator();
+ if (!en.MoveNext())
+ {
+ throw new ArgumentException("Invalid data format for object data.");
+ }
+
+ while (en.Current != AreaObjectCommand.TerminationCode)
+ {
+ if (AreaObjectCommand.IsThreeByteSpecifier(en.Current))
+ {
+ var list = new List(GetBytes(3));
+ yield return new AreaObjectCommand(
+ list[0],
+ list[1],
+ list[2]);
+ }
+ else
+ {
+ var list = new List(GetBytes(2));
+ yield return new AreaObjectCommand(
+ list[0],
+ list[1]);
+ }
+ }
+
+ IEnumerable GetBytes(int size)
+ {
+ for (var i = 0; i < size; i++)
+ {
+ yield return en.Current;
+
+ if (!en.MoveNext())
+ {
+ throw new ArgumentException("Invalid data format for object data.");
+ }
+ }
+ }
+ }
+
+ private static IEnumerable GetAreaSpriteData(
+ IEnumerable bytes)
+ {
+ using var en = bytes.GetEnumerator();
+ if (!en.MoveNext())
+ {
+ throw new ArgumentException("Invalid data format for sprite data.");
+ }
+
+ while (en.Current != AreaSpriteCommand.TerminationCode)
+ {
+ if (AreaSpriteCommand.IsThreeByteSpecifier(en.Current))
+ {
+ var list = new List(GetBytes(3));
+ yield return new AreaSpriteCommand(
+ list[0],
+ list[1],
+ list[2]);
+ }
+ else
+ {
+ var list = new List(GetBytes(2));
+ yield return new AreaSpriteCommand(
+ list[0],
+ list[1]);
+ }
+ }
+
+ IEnumerable GetBytes(int size)
+ {
+ for (var i = 0; i < size; i++)
+ {
+ yield return en.Current;
+
+ if (!en.MoveNext())
+ {
+ throw new ArgumentException("Invalid data format for sprite data.");
+ }
+ }
+ }
+ }
+}
diff --git a/src/Smas/Smb1/AreaData/AreaLoaderPointers.cs b/src/Smas/Smb1/AreaData/AreaLoaderPointers.cs
new file mode 100644
index 0000000..230acae
--- /dev/null
+++ b/src/Smas/Smb1/AreaData/AreaLoaderPointers.cs
@@ -0,0 +1,117 @@
+//
+// Copyright (c) 2022 spel werdz rite. All rights reserved. Licensed under GNU Affero
+// General Public License. See LICENSE in project root for full license
+// information, or visit https://www.gnu.org/licenses/#AGPL
+//
+
+namespace Maseya.Smas.Smb1.AreaData;
+
+public class AreaLoaderPointers
+{
+ public static readonly AreaLoaderPointers Jp10 = new(
+ baseAddress: 0x04C026);
+
+ public static readonly AreaLoaderPointers Jp11 = new(
+ baseAddress: 0x04C026);
+
+ public static readonly AreaLoaderPointers Usa = new(
+ baseAddress: 0x04C026);
+
+ public static readonly AreaLoaderPointers UsaLL = new(
+ numberOfWorldsAddress: 0x0EC37B,
+ worldLevelOffsetPointer: 0x0EC38A,
+ areaIndexTablePointer: 0x0EC392,
+ spriteAreaTypeOffsetPointer: 0x0EC3EC,
+ spriteLowBytePointer: 0x0EC3F4,
+ spriteHighBytePointer: 0x0EC3F9,
+ objectAreaTypeOffsetPointer: 0x0EC404,
+ objectLowBytePointer: 0x0EC40E,
+ objectHighBytePointer: 0x0EC413,
+ areaDataStartPointer: 0x0EC624,
+ areaDataEndPointer: 0x0EFFFF,
+ numberOfAreas: 0x47,
+ areaNumberTableSize: 0x3A);
+
+ public static readonly AreaLoaderPointers UsaPlusW = new(
+ baseAddress: 0x04C026);
+
+ public static readonly AreaLoaderPointers Eu = new(
+ baseAddress: 0x04C026);
+
+ public static readonly AreaLoaderPointers EuPlusW = new(
+ baseAddress: 0x04C026);
+
+ public static readonly AreaLoaderPointers UsaSmb1 = new(
+ baseAddress: 0x01C026);
+
+ public AreaLoaderPointers(
+ int numberOfWorldsAddress,
+ int worldLevelOffsetPointer,
+ int areaIndexTablePointer,
+ int spriteAreaTypeOffsetPointer,
+ int spriteLowBytePointer,
+ int spriteHighBytePointer,
+ int objectAreaTypeOffsetPointer,
+ int objectLowBytePointer,
+ int objectHighBytePointer,
+ int areaDataStartPointer,
+ int areaDataEndPointer,
+ int numberOfAreas = 0x22,
+ int areaNumberTableSize = 0x24)
+ {
+ NumberOfWorldsAddress = numberOfWorldsAddress;
+ WorldAreaNumberOffsetPointer = worldLevelOffsetPointer;
+ AreaNumberTablePointer = areaIndexTablePointer;
+ SpriteAreaTypeOffsetPointer = spriteAreaTypeOffsetPointer;
+ SpriteLowBytePointer = spriteLowBytePointer;
+ SpriteHighBytePointer = spriteHighBytePointer;
+ ObjectAreaTypeOffsetPointer = objectAreaTypeOffsetPointer;
+ ObjectLowBytePointer = objectLowBytePointer;
+ ObjectHighBytePointer = objectHighBytePointer;
+ AreaDataStartPointer = areaDataStartPointer;
+ AreaDataEndPointer = areaDataEndPointer;
+ NumberOfAreas = numberOfAreas;
+ AreaNumberTableSize = areaNumberTableSize;
+ }
+
+ private AreaLoaderPointers(int baseAddress)
+ : this(
+ numberOfWorldsAddress: baseAddress,
+ worldLevelOffsetPointer: baseAddress + 0x0F,
+ areaIndexTablePointer: baseAddress + 0x17,
+ spriteAreaTypeOffsetPointer: baseAddress + 0x35,
+ spriteLowBytePointer: baseAddress + 0x3D,
+ spriteHighBytePointer: baseAddress + 0x42,
+ objectAreaTypeOffsetPointer: baseAddress + 0x4D,
+ objectLowBytePointer: baseAddress + 0x6D,
+ objectHighBytePointer: baseAddress + 0x72,
+ areaDataStartPointer: baseAddress + 0x01B2,
+ areaDataEndPointer: baseAddress + 0x17DA)
+ { }
+
+ public int NumberOfWorldsAddress { get; }
+
+ public int NumberOfAreas { get; }
+
+ public int AreaNumberTableSize { get; }
+
+ public int WorldAreaNumberOffsetPointer { get; }
+
+ public int AreaNumberTablePointer { get; }
+
+ public int SpriteAreaTypeOffsetPointer { get; }
+
+ public int SpriteLowBytePointer { get; }
+
+ public int SpriteHighBytePointer { get; }
+
+ public int ObjectAreaTypeOffsetPointer { get; }
+
+ public int ObjectLowBytePointer { get; }
+
+ public int ObjectHighBytePointer { get; }
+
+ public int AreaDataStartPointer { get; }
+
+ public int AreaDataEndPointer { get; }
+}
diff --git a/src/Smas/Smb1/AreaData/AreaType.cs b/src/Smas/Smb1/AreaData/AreaType.cs
new file mode 100644
index 0000000..17f2c08
--- /dev/null
+++ b/src/Smas/Smb1/AreaData/AreaType.cs
@@ -0,0 +1,33 @@
+//
+// Copyright (c) 2022 spel werdz rite. All rights reserved. Licensed under GNU Affero
+// General Public License. See LICENSE in project root for full license
+// information, or visit https://www.gnu.org/licenses/#AGPL
+//
+
+namespace Maseya.Smas.Smb1.AreaData;
+
+///
+/// The terrain and background type of the current area.
+///
+public enum AreaType
+{
+ ///
+ /// An underwater areas where the player is swimming.
+ ///
+ Water,
+
+ ///
+ /// An above ground area. This includes sky areas too with cloud-ground.
+ ///
+ Grassland,
+
+ ///
+ /// An underground area.
+ ///
+ Underground,
+
+ ///
+ /// A castle area.
+ ///
+ Castle,
+}
diff --git a/src/Smas/Smb1/AreaData/HeaderData/AreaHeader.cs b/src/Smas/Smb1/AreaData/HeaderData/AreaHeader.cs
new file mode 100644
index 0000000..006a290
--- /dev/null
+++ b/src/Smas/Smb1/AreaData/HeaderData/AreaHeader.cs
@@ -0,0 +1,288 @@
+//
+// Copyright (c) 2022 spel werdz rite. All rights reserved. Licensed under GNU Affero
+// General Public License. See LICENSE in project root for full license
+// information, or visit https://www.gnu.org/licenses/#AGPL
+//
+
+namespace Maseya.Smas.Smb1.AreaData.HeaderData;
+
+using System;
+
+using ObjectData;
+
+///
+/// The header data for the current area.
+///
+///
+/// An area header defines how the current area should look when first entering it. It
+/// defines certain properties like start time, position, scenery, and terrain. Certain
+/// objects in the area can modify these properties too, but every area object pointer
+/// starts with two bytes that determines the header.
+///
+public struct AreaHeader : IEquatable
+{
+ ///
+ /// The size, in bytes, of this .
+ ///
+ public const int SizeOf = sizeof(ushort);
+
+ ///
+ /// Initializes a new instance of the struct.
+ ///
+ ///
+ /// The first byte of the area header.
+ ///
+ ///
+ /// The second byte of the area header.
+ ///
+ public AreaHeader(byte val1, byte val2)
+ {
+ Value1 = val1;
+ Value2 = val2;
+ }
+
+ public AreaHeader(int word)
+ : this()
+ {
+ Word = word;
+ }
+
+ public AreaHeader(
+ StartTime startTime,
+ StartYPosition startYPosition,
+ ForegroundScenery foregroundScenery,
+ AreaPlatformType areaPlatformType,
+ BackgroundScenery backgroundScenery,
+ TerrainMode terrainMode)
+ : this()
+ {
+ StartTime = startTime;
+ StartYPosition = startYPosition;
+ ForegroundScenery = foregroundScenery;
+ AreaPlatformType = areaPlatformType;
+ BackgroundScenery = backgroundScenery;
+ TerrainMode = terrainMode;
+ }
+
+ public BackgroundColorControl BackgroundColorControl
+ {
+ get
+ {
+ var value = Value1 & 0x07;
+ return value >= 4 ? (BackgroundColorControl)(value & 4) : 0;
+ }
+
+ set
+ {
+ if (value == BackgroundColorControl.DayTime)
+ {
+ Value1 &= unchecked((byte)~4);
+ }
+ else
+ {
+ Value1 |= 4;
+ ForegroundScenery = ForegroundScenery.None;
+ }
+ }
+ }
+
+ ///
+ /// Gets or sets the area foreground scenery.
+ ///
+ public ForegroundScenery ForegroundScenery
+ {
+ get
+ {
+ var value = Value1 & 7;
+ return value >= 4
+ ? ForegroundScenery.None
+ : (ForegroundScenery)value;
+ }
+
+ set
+ {
+ Value1 &= unchecked((byte)~3);
+ if (value != ForegroundScenery.None)
+ {
+ Value1 |= (byte)((int)value & 3);
+ BackgroundColorControl = BackgroundColorControl.DayTime;
+ }
+ }
+ }
+
+ ///
+ /// Gets or sets the miscellaneous platform type to use in the area.
+ ///
+ public AreaPlatformType AreaPlatformType
+ {
+ get
+ {
+ return (AreaPlatformType)(Value2 >> 6);
+ }
+
+ set
+ {
+ Value2 &= unchecked((byte)~(3 << 6));
+ Value2 |= (byte)(((int)value & 3) << 6);
+ }
+ }
+
+ ///
+ /// Gets or sets the layer 1 scenery to draw at the start of the area.
+ ///
+ public BackgroundScenery BackgroundScenery
+ {
+ get
+ {
+ return (BackgroundScenery)((Value2 >> 4) & 3);
+ }
+
+ set
+ {
+ Value2 &= unchecked((byte)~(3 << 4));
+ Value2 |= (byte)(((int)value & 3) << 4);
+ }
+ }
+
+ ///
+ /// Gets or sets the players start time when entering the area.
+ ///
+ public StartTime StartTime
+ {
+ get
+ {
+ return (StartTime)(Value1 >> 6);
+ }
+
+ set
+ {
+ Value1 &= unchecked((byte)~(3 << 6));
+ Value1 |= (byte)(((int)value & 3) << 6);
+ }
+ }
+
+ ///
+ /// Gets or sets the players start Y-position when entering the area.
+ ///
+ public StartYPosition StartYPosition
+ {
+ get
+ {
+ return (StartYPosition)((Value1 >> 3) & 7);
+ }
+
+ set
+ {
+ Value1 &= unchecked((byte)~(7 << 3));
+ Value1 |= (byte)(((int)value & 7) << 3);
+ }
+ }
+
+ public int StartYPixel
+ {
+ get
+ {
+ return StartYPosition switch
+ {
+ StartYPosition.Y00 => 0x00,
+ StartYPosition.Y20 => 0x20,
+ StartYPosition.YB0 => 0xB0,
+ StartYPosition.Y50 => 0x50,
+ StartYPosition.Alt1Y00 => 0x00,
+ StartYPosition.Alt2Y00 => 0x00,
+ StartYPosition.PipeIntroYB0 => 0xB0,
+ StartYPosition.AltPipeIntroYB0 => 0xB0,
+ _ => 0x00,
+ };
+ }
+ }
+
+ ///
+ /// Gets or sets the terrain layout to use when starting the area.
+ ///
+ public TerrainMode TerrainMode
+ {
+ get
+ {
+ return (TerrainMode)(Value2 & 0x0F);
+ }
+
+ set
+ {
+ Value2 &= unchecked((byte)~0x0F);
+ Value2 |= (byte)((int)value & 0x0F);
+ }
+ }
+
+ ///
+ /// Gets or sets the first byte of the area data.
+ ///
+ public byte Value1
+ {
+ get;
+ set;
+ }
+
+ ///
+ /// Gets or sets the second byte of the area data.
+ ///
+ public byte Value2
+ {
+ get;
+ set;
+ }
+
+ public int Word
+ {
+ get
+ {
+ return Value1 | (Value2 << 8);
+ }
+
+ set
+ {
+ Value1 = (byte)value;
+ Value2 = (byte)(value >> 8);
+ }
+ }
+
+ public static bool operator !=(AreaHeader left, AreaHeader right)
+ {
+ return !(left == right);
+ }
+
+ public static bool operator ==(AreaHeader left, AreaHeader right)
+ {
+ return left.Equals(right);
+ }
+
+ public static implicit operator int(AreaHeader header)
+ {
+ return header.Word;
+ }
+
+ public static implicit operator AreaHeader(int word)
+ {
+ return new AreaHeader(word);
+ }
+
+ public override bool Equals(object? obj)
+ {
+ return obj is AreaHeader other && Equals(other);
+ }
+
+ public bool Equals(AreaHeader other)
+ {
+ return Value1.Equals(other.Value1) && Value2.Equals(other.Value2);
+ }
+
+ public override int GetHashCode()
+ {
+ return Value1 | (Value2 << 8);
+ }
+
+ public override string ToString()
+ {
+ return $"{Value1:X2} {Value2:X2}";
+ }
+}
diff --git a/src/Smas/Smb1/AreaData/HeaderData/BackgroundColorControl.cs b/src/Smas/Smb1/AreaData/HeaderData/BackgroundColorControl.cs
new file mode 100644
index 0000000..12716ec
--- /dev/null
+++ b/src/Smas/Smb1/AreaData/HeaderData/BackgroundColorControl.cs
@@ -0,0 +1,20 @@
+//
+// Copyright (c) 2022 spel werdz rite. All rights reserved. Licensed under GNU Affero
+// General Public License. See LICENSE in project root for full license
+// information, or visit https://www.gnu.org/licenses/#AGPL
+//
+
+namespace Maseya.Smas.Smb1.AreaData.HeaderData;
+
+public enum BackgroundColorControl
+{
+ ///
+ /// Normal day time area.
+ ///
+ DayTime,
+
+ ///
+ /// Normal night time area (e.g. main area of W3-1).
+ ///
+ NightTime,
+}
diff --git a/src/Smas/Smb1/AreaData/HeaderData/BackgroundScenery.cs b/src/Smas/Smb1/AreaData/HeaderData/BackgroundScenery.cs
new file mode 100644
index 0000000..d777afd
--- /dev/null
+++ b/src/Smas/Smb1/AreaData/HeaderData/BackgroundScenery.cs
@@ -0,0 +1,33 @@
+//
+// Copyright (c) 2022 spel werdz rite. All rights reserved. Licensed under GNU Affero
+// General Public License. See LICENSE in project root for full license
+// information, or visit https://www.gnu.org/licenses/#AGPL
+//
+
+namespace Maseya.Smas.Smb1.AreaData.HeaderData;
+
+///
+/// The layer 1 scenery to draw for the area.
+///
+public enum BackgroundScenery
+{
+ ///
+ /// Use no scenery.
+ ///
+ None,
+
+ ///
+ /// Clouds in sky.
+ ///
+ Clouds,
+
+ ///
+ /// Mountains and hills on ground.
+ ///
+ MountainsAndHills,
+
+ ///
+ /// Fences and trees on ground.
+ ///
+ FenceAndTrees,
+}
diff --git a/src/Smas/Smb1/AreaData/HeaderData/ForegroundScenery.cs b/src/Smas/Smb1/AreaData/HeaderData/ForegroundScenery.cs
new file mode 100644
index 0000000..789b4a7
--- /dev/null
+++ b/src/Smas/Smb1/AreaData/HeaderData/ForegroundScenery.cs
@@ -0,0 +1,33 @@
+//
+// Copyright (c) 2022 spel werdz rite. All rights reserved. Licensed under GNU Affero
+// General Public License. See LICENSE in project root for full license
+// information, or visit https://www.gnu.org/licenses/#AGPL
+//
+
+namespace Maseya.Smas.Smb1.AreaData.HeaderData;
+
+///
+/// The layer 1 foreground to use for the current area.
+///
+public enum ForegroundScenery
+{
+ ///
+ /// No background.
+ ///
+ None,
+
+ ///
+ /// User for underwater area (e.g. main area of W2-2).
+ ///
+ Underwater,
+
+ ///
+ /// A castle wall is behind the player (e.g. main area of W8-3).
+ ///
+ CastleWall,
+
+ ///
+ /// Water or lava is at ground level (e.g. main area of W2-3).
+ ///
+ OverWater,
+}
diff --git a/src/Smas/Smb1/AreaData/HeaderData/StartTime.cs b/src/Smas/Smb1/AreaData/HeaderData/StartTime.cs
new file mode 100644
index 0000000..c1c454c
--- /dev/null
+++ b/src/Smas/Smb1/AreaData/HeaderData/StartTime.cs
@@ -0,0 +1,33 @@
+//
+// Copyright (c) 2022 spel werdz rite. All rights reserved. Licensed under GNU Affero
+// General Public License. See LICENSE in project root for full license
+// information, or visit https://www.gnu.org/licenses/#AGPL
+//
+
+namespace Maseya.Smas.Smb1.AreaData.HeaderData;
+
+///
+/// The starting time when the player enters the area.
+///
+public enum StartTime
+{
+ ///
+ /// No time. Use this for auto-walk areas.
+ ///
+ None = 0,
+
+ ///
+ /// 400 game seconds.
+ ///
+ Time400,
+
+ ///
+ /// 300 game seconds.
+ ///
+ Time300,
+
+ ///
+ /// 200 game seconds.
+ ///
+ Time200,
+}
diff --git a/src/Smas/Smb1/AreaData/HeaderData/StartYPosition.cs b/src/Smas/Smb1/AreaData/HeaderData/StartYPosition.cs
new file mode 100644
index 0000000..1e7d166
--- /dev/null
+++ b/src/Smas/Smb1/AreaData/HeaderData/StartYPosition.cs
@@ -0,0 +1,66 @@
+//
+// Copyright (c) 2022 spel werdz rite. All rights reserved. Licensed under GNU Affero
+// General Public License. See LICENSE in project root for full license
+// information, or visit https://www.gnu.org/licenses/#AGPL
+//
+
+namespace Maseya.Smas.Smb1.AreaData.HeaderData;
+
+///
+/// The player's starting Y position when entering the area.
+///
+///
+/// The names specify the absolute screen coordinates, but the descriptions specify the
+/// relative tile coordinates.
+///
+public enum StartYPosition
+{
+ ///
+ /// Y = -1
+ ///
+ Y00,
+
+ ///
+ /// Y = -1 entering from another area.
+ ///
+ Y20,
+
+ ///
+ /// Y = 10
+ ///
+ YB0,
+
+ ///
+ /// Y = 4
+ ///
+ Y50,
+
+ ///
+ /// Y = -1
+ ///
+ ///
+ /// It is not yet clear how this is different from .
+ ///
+ Alt1Y00,
+
+ ///
+ /// Y = -1
+ ///
+ ///
+ /// It is not yet clear how this is different from .
+ ///
+ Alt2Y00,
+
+ ///
+ /// Y = 10 (autowalk)
+ ///
+ PipeIntroYB0,
+
+ ///
+ /// Y = 10 (autowalk)
+ ///
+ ///
+ /// It is not yet clear how this is different form .
+ ///
+ AltPipeIntroYB0,
+}
diff --git a/src/Smas/Smb1/AreaData/HeaderData/TerrainMode.cs b/src/Smas/Smb1/AreaData/HeaderData/TerrainMode.cs
new file mode 100644
index 0000000..92f48a3
--- /dev/null
+++ b/src/Smas/Smb1/AreaData/HeaderData/TerrainMode.cs
@@ -0,0 +1,93 @@
+//
+// Copyright (c) 2022 spel werdz rite. All rights reserved. Licensed under GNU Affero
+// General Public License. See LICENSE in project root for full license
+// information, or visit https://www.gnu.org/licenses/#AGPL
+//
+
+namespace Maseya.Smas.Smb1.AreaData.HeaderData;
+
+///
+/// The floor plan to use in the area.
+///
+public enum TerrainMode
+{
+ ///
+ /// No ground.
+ ///
+ None,
+
+ ///
+ /// 2 tile high floor with no ceiling.
+ ///
+ Ceiling0Floor2,
+
+ ///
+ /// 2 tile high floor and 1 tile high ceiling.
+ ///
+ Ceiling1Floor2,
+
+ ///
+ /// 2 tile high floor and 3 tile high ceiling.
+ ///
+ Ceiling3Floor2,
+
+ ///
+ /// 2 tile high floor and 4 tile high ceiling.
+ ///
+ Ceiling4Floor2,
+
+ ///
+ /// 2 tile high floor and 8 tile high ceiling.
+ ///
+ Ceiling8Floor2,
+
+ ///
+ /// 5 tile high floor and 1 tile high ceiling.
+ ///
+ Ceiling1Floor5,
+
+ ///
+ /// 5 tile high floor and 3 tile high ceiling.
+ ///
+ Ceiling3Floor5,
+
+ ///
+ /// 5 tile high floor and 4 tile high ceiling.
+ ///
+ Ceiling4Floor5,
+
+ ///
+ /// 6 tile high floor and 1 tile high ceiling.
+ ///
+ Ceiling1Floor6,
+
+ ///
+ /// No floor and 1 tile high ceiling.
+ ///
+ Ceiling1Floor0,
+
+ ///
+ /// 6 tile high floor and 4 tile high ceiling.
+ ///
+ Ceiling4Floor6,
+
+ ///
+ /// 9 tile high floor and 1 tile high ceiling.
+ ///
+ Ceiling1Floor9,
+
+ ///
+ /// 2 tile high floor, 1 tile high ceiling, and 5 layers in the middle.
+ ///
+ Ceiling1Middle5Floor2,
+
+ ///
+ /// 2 tile high floor, 1 tile high ceiling, and 4 layers in the middle.
+ ///
+ Ceiling1Middle4Floor2,
+
+ ///
+ /// All heights have floor tile.
+ ///
+ Solid,
+}
diff --git a/src/Smas/Smb1/AreaData/ObjectData/AreaObjectCode.cs b/src/Smas/Smb1/AreaData/ObjectData/AreaObjectCode.cs
new file mode 100644
index 0000000..6fa920a
--- /dev/null
+++ b/src/Smas/Smb1/AreaData/ObjectData/AreaObjectCode.cs
@@ -0,0 +1,83 @@
+//
+// Copyright (c) 2022 spel werdz rite. All rights reserved. Licensed under GNU Affero
+// General Public License. See LICENSE in project root for full license
+// information, or visit https://www.gnu.org/licenses/#AGPL
+//
+
+namespace Maseya.Smas.Smb1.AreaData.ObjectData;
+
+public enum AreaObjectCode
+{
+ QuestionBlockPowerup,
+ QuestionBlockCoin,
+ HiddenBlockCoin,
+ HiddenBlock1UP,
+ BrickPowerup,
+ BrickBeanstalk,
+ BrickStar,
+ Brick10Coins,
+ Brick1UP,
+ SidewaysPipe,
+ UsedBlock,
+ SpringBoard,
+ JPipe,
+ FlagPole,
+ Empty,
+ Empty2,
+ AreaSpecificPlatform = 0x10,
+
+ GreenIsland = AreaSpecificPlatform | 8 | AreaPlatformType.Trees,
+ MushroomIsland = AreaSpecificPlatform | 8 | AreaPlatformType.Mushrooms,
+ Cannon = AreaSpecificPlatform | 8 | AreaPlatformType.BulletBillTurrets,
+ CloudGround = AreaSpecificPlatform | 8 | AreaPlatformType.CloudGround,
+
+ HorizontalBricks = 0x20,
+ HorizontalStones = 0x30,
+ HorizontalCoins = 0x40,
+ VerticalBricks = 0x50,
+ VerticalStones = 0x60,
+ UnenterablePipe = 0x70,
+ EnterablePipe = 0x78,
+
+ Hole = 0x0C00,
+ BalanceHorizontalRope = 0x0C10,
+ BridgeV7 = 0x0C20,
+ BridgeV8 = 0x0C30,
+ BridgeV10 = 0x0C40,
+ HoleWithWaterOrLava = 0x0C50,
+ HorizontalQuestionBlocksV3 = 0x0C60,
+ HorizontalQuestionBlocksV7 = 0x0C70,
+
+ ScreenJump = 0x0D00,
+ AltJPipe = 0x0D40,
+ AltFlagPole,
+ BowserAxe,
+ RopeForAxe,
+ BowserBridge,
+ ScrollStopWarpZone,
+ ScrollStop,
+ AltScrollStop,
+ RedCheepCheepFlying,
+ BulletBillGenerator,
+ StopGenerator,
+ LoopCommand,
+
+ TerrainAndBackgroundSceneryChange = 0x0E00,
+ ForegroundChange = 0x0E40,
+
+ RopeForLift = 0x0F00,
+ PulleyRope = 0x0F10,
+ EmptyTile = 0x0F12,
+ Castle = 0x0F20,
+ CastleCeilingCap = 0x0F28,
+ Staircase = 0x0F30,
+ CastleStairs = 0x0F32,
+ CastleRectangularCeilingTiles = 0x0F34,
+ CastleFloorRightEdge = 0x0F36,
+ CastleFloorLeftEdge = 0x0F38,
+ CastleFloorLeftWall = 0x0F3A,
+ CastleFloorRightWall = 0x0F3C,
+ VerticalSeaBlocks = 0x0F3E,
+ ExtendableJPipe = 0x0F40,
+ VerticalBalls = 0x0F50,
+}
diff --git a/src/Smas/Smb1/AreaData/ObjectData/AreaObjectCommand.cs b/src/Smas/Smb1/AreaData/ObjectData/AreaObjectCommand.cs
new file mode 100644
index 0000000..d07260e
--- /dev/null
+++ b/src/Smas/Smb1/AreaData/ObjectData/AreaObjectCommand.cs
@@ -0,0 +1,662 @@
+//
+// Copyright (c) 2022 spel werdz rite. All rights reserved. Licensed under GNU Affero
+// General Public License. See LICENSE in project root for full license
+// information, or visit https://www.gnu.org/licenses/#AGPL
+//
+
+namespace Maseya.Smas.Smb1.AreaData.ObjectData;
+
+using System;
+using System.Collections.ObjectModel;
+
+using HeaderData;
+
+using Maseya.Smas.Smb1;
+
+public struct AreaObjectCommand : IEquatable
+{
+ ///
+ /// The object command to read that defined the end of the area object data.
+ ///
+ public const byte TerminationCode = 0xFD;
+
+ public static readonly ReadOnlyCollection ValidCodes =
+ new(new AreaObjectCode[]
+ {
+ AreaObjectCode.QuestionBlockPowerup,
+ AreaObjectCode.QuestionBlockCoin,
+ AreaObjectCode.HiddenBlockCoin,
+ AreaObjectCode.HiddenBlock1UP,
+ AreaObjectCode.BrickPowerup,
+ AreaObjectCode.BrickBeanstalk,
+ AreaObjectCode.BrickStar,
+ AreaObjectCode.Brick10Coins,
+ AreaObjectCode.Brick1UP,
+ AreaObjectCode.SidewaysPipe,
+ AreaObjectCode.UsedBlock,
+ AreaObjectCode.SpringBoard,
+ AreaObjectCode.JPipe,
+ AreaObjectCode.FlagPole,
+ AreaObjectCode.Empty,
+ AreaObjectCode.Empty2,
+ AreaObjectCode.AreaSpecificPlatform,
+ AreaObjectCode.HorizontalBricks,
+ AreaObjectCode.HorizontalStones,
+ AreaObjectCode.HorizontalCoins,
+ AreaObjectCode.VerticalBricks,
+ AreaObjectCode.VerticalStones,
+ AreaObjectCode.UnenterablePipe,
+ AreaObjectCode.EnterablePipe,
+ AreaObjectCode.Hole,
+ AreaObjectCode.BalanceHorizontalRope,
+ AreaObjectCode.BridgeV7,
+ AreaObjectCode.BridgeV8,
+ AreaObjectCode.BridgeV10,
+ AreaObjectCode.HoleWithWaterOrLava,
+ AreaObjectCode.HorizontalQuestionBlocksV3,
+ AreaObjectCode.HorizontalQuestionBlocksV7,
+ AreaObjectCode.AltJPipe,
+ AreaObjectCode.AltFlagPole,
+ AreaObjectCode.BowserAxe,
+ AreaObjectCode.RopeForAxe,
+ AreaObjectCode.BowserBridge,
+ AreaObjectCode.ScrollStopWarpZone,
+ AreaObjectCode.ScrollStop,
+ AreaObjectCode.AltScrollStop,
+ AreaObjectCode.RedCheepCheepFlying,
+ AreaObjectCode.BulletBillGenerator,
+ AreaObjectCode.StopGenerator,
+ AreaObjectCode.LoopCommand,
+ AreaObjectCode.TerrainAndBackgroundSceneryChange,
+ AreaObjectCode.ForegroundChange,
+ AreaObjectCode.RopeForLift,
+ AreaObjectCode.PulleyRope,
+ AreaObjectCode.EmptyTile,
+ AreaObjectCode.Castle,
+ AreaObjectCode.CastleCeilingCap,
+ AreaObjectCode.Staircase,
+ AreaObjectCode.CastleStairs,
+ AreaObjectCode.CastleRectangularCeilingTiles,
+ AreaObjectCode.CastleFloorRightEdge,
+ AreaObjectCode.CastleFloorLeftEdge,
+ AreaObjectCode.CastleFloorLeftWall,
+ AreaObjectCode.CastleFloorRightWall,
+ AreaObjectCode.VerticalSeaBlocks,
+ AreaObjectCode.ExtendableJPipe,
+ AreaObjectCode.VerticalBalls,
+ });
+
+ public AreaObjectCommand(byte value1, byte value2, byte value3 = 0)
+ {
+ Value1 = value1;
+ Value2 = value2;
+ Value3 = (byte)(((value1 & 0x0F) == 0x0F) ? value3 : 0);
+ }
+
+ public byte Value1
+ {
+ get;
+ set;
+ }
+
+ public byte Value2
+ {
+ get;
+ set;
+ }
+
+ public byte Value3
+ {
+ get;
+ set;
+ }
+
+ ///
+ /// The size, in bytes, of this .
+ ///
+ public int Size
+ {
+ get
+ {
+ return IsThreeByteCommand ? 3 : 2;
+ }
+ }
+
+ public int X
+ {
+ get
+ {
+ return Value1 >> 4;
+ }
+
+ set
+ {
+ Value1 &= 0x0F;
+ Value1 |= (byte)((value & 0x0F) << 4);
+ }
+ }
+
+ public bool HasYCoord
+ {
+ get
+ {
+ var y = Value1 & 0x0F;
+ return y is < 0x0C or 0x0F;
+ }
+ }
+
+ public int Y
+ {
+ get
+ {
+ return IsThreeByteCommand ? Value2 >> 4 : Value1 & 0x0F;
+ }
+
+ set
+ {
+ if (IsThreeByteCommand)
+ {
+ Value2 &= 0x0F;
+ Value2 |= (byte)((value & 0x0F) << 4);
+ }
+ else
+ {
+ Value1 &= 0xF0;
+ Value1 |= (byte)(value & 0x0F);
+ }
+ }
+ }
+
+ public bool ScreenFlag
+ {
+ get
+ {
+ return ((IsThreeByteCommand ? Value3 : Value2) & 0x80) != 0;
+ }
+
+ set
+ {
+ var mask = (byte)(value ? 0x80 : 0);
+ if (IsThreeByteCommand)
+ {
+ Value3 &= 0x7F;
+ Value3 |= mask;
+ }
+ else
+ {
+ Value2 &= 0x7F;
+ Value2 |= mask;
+ }
+ }
+ }
+
+ public int BaseCommand
+ {
+ get
+ {
+ return (IsThreeByteCommand ? Value3 : Value2) & 0x7F;
+ }
+
+ set
+ {
+ if (IsThreeByteCommand)
+ {
+ Value3 &= 0x80;
+ Value3 |= (byte)(value & 0x7F);
+ }
+ else
+ {
+ Value2 &= 0x80;
+ Value2 |= (byte)(value & 0x7F);
+ }
+ }
+ }
+
+ public int Command
+ {
+ get
+ {
+ return (BaseCommand >> 4) & 7;
+ }
+
+ set
+ {
+ BaseCommand &= 0x8F;
+ BaseCommand |= (byte)((value & 7) << 4);
+ }
+ }
+
+ public int Parameter
+ {
+ get
+ {
+ return Value2 & 0x0F;
+ }
+
+ set
+ {
+ Value2 &= 0xF0;
+ Value2 |= (byte)(value & 0x0F);
+ }
+ }
+
+ public AreaObjectCode Code
+ {
+ get
+ {
+ return (Value1 & 0x0F) switch
+ {
+ 0x0F => (AreaObjectCode)(0xF00 | BaseCommand),
+ 0x0C => (AreaObjectCode)(0xC00 | (Command << 4)),
+ 0x0D => (Command & ~1) == 0
+ ? AreaObjectCode.ScreenJump
+ : (AreaObjectCode)(0xD00 | (Command << 4) | Parameter),
+ 0x0E => (AreaObjectCode)(0xE00 | ((Command & 4) << 4)),
+ _ => Command switch
+ {
+ 0 => (AreaObjectCode)Parameter,
+ 7 => Parameter < 8
+ ? AreaObjectCode.UnenterablePipe
+ : AreaObjectCode.EnterablePipe,
+ _ => (AreaObjectCode)(Command << 4),
+ },
+ };
+ }
+ }
+
+ public bool IsExtendableObject
+ {
+ get
+ {
+ return Code.IsExtendableObject();
+ }
+ }
+
+ public bool IsTerrainAndBackgroundChange
+ {
+ get
+ {
+ return Code == AreaObjectCode.TerrainAndBackgroundSceneryChange;
+ }
+ }
+
+ public bool IsForegroundChange
+ {
+ get
+ {
+ return Code == AreaObjectCode.ForegroundChange;
+ }
+ }
+
+ public int Length
+ {
+ get
+ {
+ return Code switch
+ {
+ AreaObjectCode.ScreenJump => Value2 & 0x1F,
+ AreaObjectCode.EnterablePipe or
+ AreaObjectCode.UnenterablePipe => Parameter & 7,
+ _ => IsExtendableObject ? Parameter : 0,
+ };
+ }
+
+ set
+ {
+ switch (Code)
+ {
+ case AreaObjectCode.ScreenJump:
+ Value2 &= 0xE0;
+ Value2 |= (byte)(value & 0x1F);
+ break;
+
+ case AreaObjectCode.EnterablePipe:
+ case AreaObjectCode.UnenterablePipe:
+ Parameter &= 0xF8;
+ Parameter |= (byte)(value & 7);
+ break;
+
+ default:
+ if (IsExtendableObject)
+ {
+ Parameter &= 0xF0;
+ Parameter |= (byte)(value & 0x0F);
+ }
+
+ break;
+ }
+ }
+ }
+
+ public ForegroundScenery ForegroundScenery
+ {
+ get
+ {
+ return (ForegroundScenery)(Code == AreaObjectCode.ForegroundChange
+ ? Parameter & 7
+ : 0);
+ }
+
+ set
+ {
+ if (Code == AreaObjectCode.ForegroundChange)
+ {
+ Parameter &= 0xF8;
+ Parameter |= (byte)((int)value & 7);
+ }
+ }
+ }
+
+ public TerrainMode TerrainMode
+ {
+ get
+ {
+ return (TerrainMode)(Code == AreaObjectCode.TerrainAndBackgroundSceneryChange
+ ? BaseCommand & 0x0F
+ : 0);
+ }
+
+ set
+ {
+ if (Code == AreaObjectCode.TerrainAndBackgroundSceneryChange)
+ {
+ BaseCommand &= 0xF0;
+ BaseCommand |= (int)value & 0x0F;
+ }
+ }
+ }
+
+ public BackgroundScenery BackgroundScenery
+ {
+ get
+ {
+ return (BackgroundScenery)(Code == AreaObjectCode.TerrainAndBackgroundSceneryChange
+ ? (BaseCommand >> 4) & 3
+ : 0);
+ }
+
+ set
+ {
+ if (Code == AreaObjectCode.TerrainAndBackgroundSceneryChange)
+ {
+ BaseCommand &= 0xCF;
+ BaseCommand |= ((int)value & 3) << 4;
+ }
+ }
+ }
+
+ public string BaseName
+ {
+ get
+ {
+ return Code.BaseName();
+ }
+ }
+
+ public bool IsThreeByteCommand
+ {
+ get
+ {
+ return IsThreeByteSpecifier(Value1);
+ }
+ }
+
+ public bool IsValid
+ {
+ get
+ {
+ return Value1 != 0xFD
+ && (IsThreeByteCommand || Value3 == 0);
+ }
+ }
+
+ public string HexString
+ {
+ get
+ {
+ return $"{Value1:X2} {Value2:X2}"
+ + (IsThreeByteCommand ? $" {Value3:X2}" : String.Empty);
+ }
+ }
+
+ public static bool operator ==(AreaObjectCommand left, AreaObjectCommand right)
+ {
+ return left.Equals(right);
+ }
+
+ public static bool operator !=(AreaObjectCommand left, AreaObjectCommand right)
+ {
+ return !(left == right);
+ }
+
+ public static bool IsThreeByteSpecifier(int coordinates)
+ {
+ return (coordinates & 0x0F) == 0x0F;
+ }
+
+ public string FullName(AreaPlatformType areaPlatformType)
+ {
+ var length = Parameter + 1;
+
+ switch (Code)
+ {
+ case AreaObjectCode.QuestionBlockPowerup:
+ return "Question Block (Powerup)";
+
+ case AreaObjectCode.QuestionBlockCoin:
+ return "Question Block (Coin)";
+
+ case AreaObjectCode.HiddenBlockCoin:
+ return "Hidden Block (Coin)";
+
+ case AreaObjectCode.HiddenBlock1UP:
+ return "Hidden Block (1UP)";
+
+ case AreaObjectCode.BrickPowerup:
+ return "Brick (Powerup)";
+
+ case AreaObjectCode.BrickBeanstalk:
+ return "Brick (Beanstalk)";
+
+ case AreaObjectCode.BrickStar:
+ return "Brick (Star)";
+
+ case AreaObjectCode.Brick10Coins:
+ return "Brick (10 Coins)";
+
+ case AreaObjectCode.Brick1UP:
+ return "Brick (1UP)";
+
+ case AreaObjectCode.SidewaysPipe:
+ return "Sideways Pipe Cap";
+
+ case AreaObjectCode.UsedBlock:
+ return "Used Block";
+
+ case AreaObjectCode.SpringBoard:
+ return "Spring Board";
+
+ case AreaObjectCode.JPipe:
+ case AreaObjectCode.AltJPipe:
+ return "J-Pipe";
+
+ case AreaObjectCode.FlagPole:
+ case AreaObjectCode.AltFlagPole:
+ return "Flag Pole";
+
+ case AreaObjectCode.Empty:
+ case AreaObjectCode.Empty2:
+ return "Nothing";
+
+ case AreaObjectCode.AreaSpecificPlatform:
+ switch (areaPlatformType)
+ {
+ case AreaPlatformType.Trees:
+ return $"Tree Top Platform (Width={length})";
+
+ case AreaPlatformType.Mushrooms:
+ return $"Mushroom Platform (Width={length})";
+
+ case AreaPlatformType.BulletBillTurrets:
+ return $"Bullet Bill Shooter (Height={length})";
+
+ case AreaPlatformType.CloudGround:
+ return $"Cloud Ground (Width={length})";
+
+ default:
+ break;
+ }
+
+ break;
+
+ case AreaObjectCode.HorizontalBricks:
+ return $"Horizontal Bricks (Width={length})";
+
+ case AreaObjectCode.HorizontalStones:
+ return $"Horizontal Blocks (Width={length})";
+
+ case AreaObjectCode.HorizontalCoins:
+ return $"Horizontal Coins (Width={length})";
+
+ case AreaObjectCode.VerticalBricks:
+ return $"Vertical Bricks (Height={length})";
+
+ case AreaObjectCode.VerticalStones:
+ return $"Vertical Blocks (Height={length})";
+
+ case AreaObjectCode.UnenterablePipe:
+ return $"Unenterable Pipe (Height={length})";
+
+ case AreaObjectCode.EnterablePipe:
+ return $"Enterable Pipe (Height={length})";
+
+ case AreaObjectCode.Hole:
+ return $"Hole (Width={length})";
+
+ case AreaObjectCode.BalanceHorizontalRope:
+ return $"Pulley Platforms (Width={length})";
+
+ case AreaObjectCode.BridgeV7:
+ return $"Rope Bridge (Y=7, Width={length})";
+
+ case AreaObjectCode.BridgeV8:
+ return $"Rope Bridge (Y=8, Width={length})";
+
+ case AreaObjectCode.BridgeV10:
+ return $"Rope Bridge (Y=10, Width={length})";
+
+ case AreaObjectCode.HoleWithWaterOrLava:
+ return $"Hole with water or lava (Width={length})";
+
+ case AreaObjectCode.HorizontalQuestionBlocksV3:
+ return $"Row of Coin Blocks (Y=3, Width={length})";
+
+ case AreaObjectCode.HorizontalQuestionBlocksV7:
+ return $"Row of Coin Blocks (Y=7, Width={length})";
+
+ case AreaObjectCode.ScreenJump:
+ return $"Skip to screen 0x{BaseCommand:X2}";
+
+ case AreaObjectCode.BowserAxe:
+ return $"Bowser Axe";
+
+ case AreaObjectCode.BowserBridge:
+ return $"Bowser Bridge";
+
+ case AreaObjectCode.ScrollStopWarpZone:
+ return $"Scroll Stop (Warp Zone)";
+
+ case AreaObjectCode.ScrollStop:
+ case AreaObjectCode.AltScrollStop:
+ return $"Scroll Stop";
+
+ case AreaObjectCode.RedCheepCheepFlying:
+ return $"Generator: Red flying cheep-cheeps";
+
+ case AreaObjectCode.BulletBillGenerator:
+ return $"Generator: Bullet Bills";
+
+ case AreaObjectCode.StopGenerator:
+ return $"Stop Generator (also stops Lakitus)";
+
+ case AreaObjectCode.LoopCommand:
+ return $"Screen Loop Command";
+
+ case AreaObjectCode.TerrainAndBackgroundSceneryChange:
+ return "Brick and scenery change";
+
+ case AreaObjectCode.ForegroundChange:
+ return "Foreground Change";
+
+ case AreaObjectCode.RopeForLift:
+ return "Rope for platform lifts";
+
+ case AreaObjectCode.PulleyRope:
+ return $"Rope for pulley platforms (Height={length})";
+
+ case AreaObjectCode.EmptyTile:
+ return "Empty tile";
+
+ case AreaObjectCode.Castle:
+ return "Castle";
+
+ case AreaObjectCode.CastleCeilingCap:
+ return "Castle Object: Ceiling Cap Tile";
+
+ case AreaObjectCode.Staircase:
+ return $"Staircase (Width={length})";
+
+ case AreaObjectCode.CastleStairs:
+ return "Castle Object: Descending Stairs";
+
+ case AreaObjectCode.CastleRectangularCeilingTiles:
+ return "Castle Object: Rectangular Ceiling Tiles";
+
+ case AreaObjectCode.CastleFloorRightEdge:
+ return "Castle Object: Right-Facing Wall To Floor";
+
+ case AreaObjectCode.CastleFloorLeftEdge:
+ return "Castle Object: Left-Facing Wall To Floor";
+
+ case AreaObjectCode.CastleFloorLeftWall:
+ return "Castle Object: Left-Facing Wall";
+
+ case AreaObjectCode.CastleFloorRightWall:
+ return "Castle Object: Right-Facing Wall";
+
+ case AreaObjectCode.VerticalSeaBlocks:
+ return $"Vertical Sea Blocks (Height={length})";
+
+ case AreaObjectCode.ExtendableJPipe:
+ return $"Extendable J-Pipe (Height={length})";
+
+ case AreaObjectCode.VerticalBalls:
+ return $"Vertical Climbing Balls (Height={length})";
+ }
+
+ return $"Unknown command: {this}";
+ }
+
+ public override bool Equals(object? obj)
+ {
+ return obj is AreaObjectCommand other && Equals(other);
+ }
+
+ public bool Equals(AreaObjectCommand other)
+ {
+ return Value1.Equals(other.Value1) && Value2.Equals(other.Value2)
+ && (!IsThreeByteCommand || Value3.Equals(other.Value3));
+ }
+
+ public override int GetHashCode()
+ {
+ return IsThreeByteCommand
+ ? HashCode.Combine(Value1, Value2, Value3)
+ : HashCode.Combine(Value1, Value2);
+ }
+
+ public override string ToString()
+ {
+ return $"({X}, {Y}): {BaseName}";
+ }
+}
diff --git a/src/Smas/Smb1/AreaData/ObjectData/AreaObjectParser.cs b/src/Smas/Smb1/AreaData/ObjectData/AreaObjectParser.cs
new file mode 100644
index 0000000..93f09af
--- /dev/null
+++ b/src/Smas/Smb1/AreaData/ObjectData/AreaObjectParser.cs
@@ -0,0 +1,1365 @@
+//
+// Copyright (c) 2022 spel werdz rite. All rights reserved. Licensed under GNU Affero
+// General Public License. See LICENSE in project root for full license
+// information, or visit https://www.gnu.org/licenses/#AGPL
+//
+
+namespace Maseya.Smas.Smb1.AreaData.ObjectData;
+
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+
+using HeaderData;
+
+public class AreaObjectParser
+{
+ ///
+ /// The default size of the object data buffer. This field is constant.
+ ///
+ public const int DefaultBufferSize = 5;
+
+ public AreaObjectParser(
+ AreaObjectRenderer areaObjectRenderer,
+ IList areaObjectData,
+ int bufferSize = DefaultBufferSize)
+ {
+ AreaObjectRenderer = areaObjectRenderer;
+
+ BufferSize = bufferSize >= 0
+ ? bufferSize
+ : throw new ArgumentOutOfRangeException(nameof(bufferSize));
+
+ AreaData = new Collection(areaObjectData);
+
+ PulleyRopeTileTable = areaObjectRenderer.PulleyRopeTileTable;
+ JPipeTilesTable1 = areaObjectRenderer.JPipeTilesTable1;
+ JPipeTilesTable2 = areaObjectRenderer.JPipeTilesTable2;
+ JPipeTilesTable3 = areaObjectRenderer.JPipeTilesTable3;
+ JPipeTilesTable4 = areaObjectRenderer.JPipeTilesTable4;
+ PipeTileTable = areaObjectRenderer.PipeTileTable;
+ WaterSurfaceTileTable = areaObjectRenderer.WaterSurfaceTileTable;
+ CoinRowTileTable = areaObjectRenderer.CoinRowTileTable;
+ BrickRowTileTable = areaObjectRenderer.BrickRowTileTable;
+ StoneRowTileTable = areaObjectRenderer.StoneRowTileTable;
+ SingleTileObjectTable = areaObjectRenderer.SingleTileObjectTable;
+ CastleTileTable = areaObjectRenderer.CastleTileTable;
+ StoneStairYTable = areaObjectRenderer.StoneStairYTable;
+ StoneStairHeightTable = areaObjectRenderer.StoneStairHeightTable;
+ JPipeTilesTable5 = areaObjectRenderer.JPipeTilesTable5;
+ JPipeTilesTable6 = areaObjectRenderer.JPipeTilesTable6;
+ JPipeTilesTable7 = areaObjectRenderer.JPipeTilesTable7;
+
+ LengthBuffer = new int[BufferSize];
+ IndexBuffer = new int[BufferSize];
+ TreePlatformProperties = new bool[BufferSize];
+ MushroomPlatformCenterCoordinate = new int[BufferSize];
+ RenderCommands = new Dictionary()
+ {
+ { AreaObjectCode.EnterablePipe, Pipe },
+ { AreaObjectCode.AreaSpecificPlatform, AreaSpecificPlatform },
+ { AreaObjectCode.HorizontalBricks, RowOfBricks },
+ { AreaObjectCode.HorizontalStones, RowOfStones },
+ { AreaObjectCode.HorizontalCoins, RowOfCoins },
+ { AreaObjectCode.VerticalBricks, ColumnOfBricks },
+ { AreaObjectCode.VerticalStones, ColumnOfStones },
+ { AreaObjectCode.UnenterablePipe, Pipe },
+ { AreaObjectCode.Hole, Hole },
+ { AreaObjectCode.BalanceHorizontalRope, BalanceHorizontalRope },
+ { AreaObjectCode.BridgeV7, HighBridge },
+ { AreaObjectCode.BridgeV8, MidBridge },
+ { AreaObjectCode.BridgeV10, LowBridge },
+ { AreaObjectCode.HoleWithWaterOrLava, HoleWithWaterOrLava },
+ { AreaObjectCode.HorizontalQuestionBlocksV3, HighRowOfCoinBlocks },
+ { AreaObjectCode.HorizontalQuestionBlocksV7, LowRowOfCoinBlocks },
+ { AreaObjectCode.QuestionBlockPowerup, ItemBlock },
+ { AreaObjectCode.QuestionBlockCoin, ItemBlock },
+ { AreaObjectCode.HiddenBlockCoin, ItemBlock },
+ { AreaObjectCode.HiddenBlock1UP, AreaTypeBlock },
+ { AreaObjectCode.BrickPowerup, AreaTypeBlock },
+ { AreaObjectCode.BrickBeanstalk, AreaTypeBlock },
+ { AreaObjectCode.BrickStar, AreaTypeBlock },
+ { AreaObjectCode.Brick10Coins, AreaTypeBlock },
+ { AreaObjectCode.Brick1UP, AreaTypeBlock },
+ { AreaObjectCode.SidewaysPipe, SidewaysPipe },
+ { AreaObjectCode.UsedBlock, FireBarBlock },
+ { AreaObjectCode.SpringBoard, SpringBoard },
+ { AreaObjectCode.JPipe, JPipe },
+ { AreaObjectCode.AltJPipe, JPipe },
+ { AreaObjectCode.FlagPole, FlagPole },
+ { AreaObjectCode.AltFlagPole, FlagPole },
+ { AreaObjectCode.BowserAxe, BowserAxe },
+ { AreaObjectCode.RopeForAxe, RopeForAxe },
+ { AreaObjectCode.BowserBridge, BowserBridge },
+ { AreaObjectCode.ForegroundChange, ForegroundChange },
+ { AreaObjectCode.TerrainAndBackgroundSceneryChange, TerrainModifier },
+ { AreaObjectCode.RopeForLift, RopeForLift },
+ { AreaObjectCode.PulleyRope, PulleyRope },
+ { AreaObjectCode.Castle, Castle },
+ { AreaObjectCode.CastleCeilingCap, CastleCeilingCap },
+ { AreaObjectCode.Staircase, StoneStairs },
+ { AreaObjectCode.CastleStairs, CastleDescendingSteps },
+ { AreaObjectCode.CastleRectangularCeilingTiles, CastleRectangularCeilingTiles },
+ { AreaObjectCode.CastleFloorRightEdge, CastleFloorRightEdge },
+ { AreaObjectCode.CastleFloorLeftEdge, CastleFloorLeftEdge },
+ { AreaObjectCode.CastleFloorLeftWall, CastleFloorLeftWall },
+ { AreaObjectCode.CastleFloorRightWall, CastleFloorRightWall },
+ { AreaObjectCode.VerticalSeaBlocks, VerticalSeaBlocks },
+ { AreaObjectCode.ExtendableJPipe, ExtendableJPipe },
+ { AreaObjectCode.VerticalBalls, VerticalClimbingObject },
+ };
+ }
+
+ public AreaObjectRenderer AreaObjectRenderer { get; }
+
+ public byte[] PulleyRopeTileTable { get; }
+
+ public byte[] JPipeTilesTable1 { get; }
+
+ public byte[] JPipeTilesTable2 { get; }
+
+ public byte[] JPipeTilesTable3 { get; }
+
+ public byte[] JPipeTilesTable4 { get; }
+
+ public byte[] PipeTileTable { get; }
+
+ public byte[] WaterSurfaceTileTable { get; }
+
+ public byte[] CoinRowTileTable { get; }
+
+ public byte[] BrickRowTileTable { get; }
+
+ public byte[] StoneRowTileTable { get; }
+
+ public byte[] SingleTileObjectTable { get; }
+
+ public byte[] CastleTileTable { get; }
+
+ public byte[] StoneStairYTable { get; }
+
+ public byte[] StoneStairHeightTable { get; }
+
+ public byte[] JPipeTilesTable5 { get; }
+
+ public byte[] JPipeTilesTable6 { get; }
+
+ public byte[] JPipeTilesTable7 { get; }
+
+ private AreaType AreaType
+ {
+ get
+ {
+ return AreaObjectRenderer.AreaType;
+ }
+ }
+
+ ///
+ /// Gets the size of the object data buffers. The default value is set to .
+ ///
+ private int BufferSize
+ {
+ get;
+ }
+
+ ///
+ /// Gets the X register, which represents the current index of the object data
+ /// buffer the is on.
+ ///
+ private int CurrentBufferIndex
+ {
+ get;
+ set;
+ }
+
+ ///
+ /// Gets the Y register, which represents the current index of the area object data
+ /// collection the is on.
+ ///
+ private int AreaDataIndex
+ {
+ get;
+ set;
+ }
+
+ ///
+ /// Gets $FA, which represents the pointer to the area object data.
+ ///
+ private Collection AreaData
+ {
+ get;
+ }
+
+ private AreaObjectCommand CurrentObjectCommand
+ {
+ get
+ {
+ return AreaData[AreaDataIndex];
+ }
+
+ set
+ {
+ AreaData[AreaDataIndex] = value;
+ }
+ }
+
+ private AreaObjectCommand CurrentBufferObject
+ {
+ get
+ {
+ return AreaData[CurrentBufferObjectIndex];
+ }
+
+ set
+ {
+ AreaData[CurrentBufferObjectIndex] = value;
+ }
+ }
+
+ private AreaObjectCode CurrentObjectCode
+ {
+ get
+ {
+ return CurrentObjectCommand.Code;
+ }
+ }
+
+ private bool IsScreenFlag
+ {
+ get
+ {
+ return CurrentObjectCommand.ScreenFlag;
+ }
+ }
+
+ private bool IsScreenJumpCommand
+ {
+ get
+ {
+ return CurrentObjectCode == AreaObjectCode.ScreenJump;
+ }
+ }
+
+ private AreaHeader CurrentHeader
+ {
+ get
+ {
+ return AreaObjectRenderer.CurrentHeader;
+ }
+
+ set
+ {
+ AreaObjectRenderer.CurrentHeader = value;
+ }
+ }
+
+ ///
+ /// Gets $06A1, which represents the tile data buffer.
+ ///
+ private int[] TileBuffer
+ {
+ get
+ {
+ return AreaObjectRenderer.TileBuffer;
+ }
+ }
+
+ ///
+ /// Gets $1300, which represents the current buffer object's width.
+ ///
+ private int[] LengthBuffer
+ {
+ get;
+ }
+
+ ///
+ /// Gets $1300,x.
+ ///
+ private int CurrentBufferObjectWidth
+ {
+ get
+ {
+ return LengthBuffer[CurrentBufferIndex];
+ }
+
+ set
+ {
+ LengthBuffer[CurrentBufferIndex] = value;
+ }
+ }
+
+ ///
+ /// Gets a value that determines whether has
+ /// been set to a valid value.
+ ///
+ private bool CurrentBufferEnabled
+ {
+ get
+ {
+ return CurrentBufferObjectWidth >= 0;
+ }
+ }
+
+ ///
+ /// Gets $1305, which represents the area object data index
+ ///
+ private int[] IndexBuffer
+ {
+ get;
+ }
+
+ ///
+ /// Gets $130F, which represents extra properties for the respective buffer object.
+ ///
+ private bool[] TreePlatformProperties
+ {
+ get;
+ }
+
+ private bool ObjectHasSpecialProperties
+ {
+ get
+ {
+ return TreePlatformProperties[CurrentBufferIndex];
+ }
+
+ set
+ {
+ TreePlatformProperties[CurrentBufferIndex] = value;
+ }
+ }
+
+ private int[] MushroomPlatformCenterCoordinate
+ {
+ get;
+ }
+
+ private int ObjectSpecialCoordinate
+ {
+ get
+ {
+ return MushroomPlatformCenterCoordinate[CurrentBufferIndex];
+ }
+
+ set
+ {
+ MushroomPlatformCenterCoordinate[CurrentBufferIndex] = value;
+ }
+ }
+
+ ///
+ /// Gets $1305,x
+ ///
+ private int CurrentBufferObjectIndex
+ {
+ get
+ {
+ return IndexBuffer[CurrentBufferIndex];
+ }
+
+ set
+ {
+ IndexBuffer[CurrentBufferIndex] = value;
+ }
+ }
+
+ ///
+ /// Gets or sets $0725, which represents the screen the renderer is currently on.
+ ///
+ private int CurrentRenderingScreen
+ {
+ get
+ {
+ return AreaObjectRenderer.CurrentRenderingScreen;
+ }
+
+ set
+ {
+ AreaObjectRenderer.CurrentRenderingScreen = value;
+ }
+ }
+
+ ///
+ /// Gets or sets $0726, which represents the X-coordinate of the current screen the
+ /// renderer is currently on.
+ ///
+ private int CurrentRenderingScreenX
+ {
+ get
+ {
+ return AreaObjectRenderer.CurrentRenderingScreenX;
+ }
+
+ set
+ {
+ AreaObjectRenderer.CurrentRenderingScreenX = value;
+ }
+ }
+
+ ///
+ /// Gets or sets the full X-coordinate the renderer is currently on.
+ ///
+ private int CurrentRenderingX
+ {
+ get
+ {
+ return AreaObjectRenderer.CurrentRenderingX;
+ }
+
+ set
+ {
+ AreaObjectRenderer.CurrentRenderingX = value;
+ }
+ }
+
+ ///
+ /// Gets or sets $072A, which represents the screen the area object data starts on.
+ ///
+ private int CurrentObjectScreen
+ {
+ get;
+ set;
+ }
+
+ ///
+ /// Gets or sets $072B, which represents a flag that determines whether a page jump
+ /// command has been activated for this current rendering pass.
+ ///
+ private bool IsScreenJumpSet
+ {
+ get;
+ set;
+ }
+
+ ///
+ /// Gets or sets $072C, which represents the saved area data index.
+ ///
+ private int StoredAreaDataIndex
+ {
+ get;
+ set;
+ }
+
+ private bool IsEndOfArea
+ {
+ get
+ {
+ return AreaDataIndex == AreaData.Count;
+ }
+ }
+
+ ///
+ /// Gets $0729, which determines whether the current object is behind the rendering screen.
+ ///
+ private bool IsObjectBehindRenderer
+ {
+ get;
+ set;
+ }
+
+ private int ObjectParameter
+ {
+ get;
+ set;
+ }
+
+ private Dictionary RenderCommands
+ {
+ get;
+ }
+
+ ///
+ /// Clears all buffers in . This should be called every
+ /// time a new screen is going to be rendered.
+ ///
+ public void ResetBuffer()
+ {
+ for (var i = BufferSize; --i >= 0;)
+ {
+ LengthBuffer[i] = -1;
+ }
+ }
+
+ ///
+ /// Parses the area data for object at . The result
+ /// is stored to .
+ ///
+ public void ParseAreaData()
+ {
+ do
+ {
+ LoadBufferData();
+ }
+ while (IsObjectBehindRenderer);
+ }
+
+ private static bool CanWriteTile(int tile, int currentTile)
+ {
+ return currentTile switch
+ {
+ 0 => true,
+
+ 0x1B or
+ 0x1E or
+ 0x46 or
+ 0x4A => false,
+
+ 0x56 or
+ 0x57 => tile != 0x50,
+
+ _ => currentTile <= 0xE7,
+ };
+ }
+
+ private void LoadBufferData()
+ {
+ CurrentBufferIndex = BufferSize - 1;
+
+ do
+ {
+ DecodeBufferData();
+ }
+ while (MoveToNextBuffer());
+ }
+
+ private void DecodeBufferData()
+ {
+ // Return to the current area index if a buffer changed it at any time.
+ AreaDataIndex = StoredAreaDataIndex;
+
+ IsObjectBehindRenderer = false;
+
+ if (IsRenderableObject())
+ {
+ // If we have an object to render, then let's render it.
+ DecodeAreaData();
+ }
+ else
+ {
+ // Go to next object if current one cannot be rendered.
+ IncrementAreaDataIndex();
+ }
+ }
+
+ ///
+ /// Gets a value that determines whether we can decode the current object or if we
+ /// should instead read the next one.
+ ///
+ ///
+ /// if there is an object in the buffer that can be rendered
+ /// or if there is no more area data to parse; otherwise, .
+ ///
+ private bool IsRenderableObject()
+ {
+ // If we're at the end of the array, attempt to decode any buffer objects. Or,
+ // if there is a currently set buffer object, decode it right away.
+ if (IsEndOfArea || CurrentBufferEnabled)
+ {
+ return true;
+ }
+
+ // Increment screen if we encounter a screen flag.
+ if (IsScreenFlag && !IsScreenJumpSet)
+ {
+ CurrentObjectScreen++;
+ IsScreenJumpSet = true;
+ }
+
+ // Update screen if we encounter a screen skip object.
+ if (IsScreenJumpCommand && !IsScreenJumpSet)
+ {
+ CurrentObjectScreen = CurrentObjectCommand.BaseCommand;
+ IsScreenJumpSet = true;
+ return false;
+ }
+
+ // Object is behind render when its page before rendering page.
+ IsObjectBehindRenderer = CurrentObjectScreen < CurrentRenderingScreen;
+
+ // Decode area data of object is on or in front of renderer.
+ return !IsObjectBehindRenderer;
+ }
+
+ ///
+ /// Update the area data index and resets for the
+ /// next object.
+ ///
+ private void IncrementAreaDataIndex()
+ {
+ StoredAreaDataIndex++;
+ IsScreenJumpSet = false;
+ }
+
+ ///
+ /// Moves to next buffer index and updates the length of the current buffer it is enabled.
+ ///
+ ///
+ /// if there is another buffer to read; otherwise, .
+ ///
+ private bool MoveToNextBuffer()
+ {
+ if (CurrentBufferEnabled)
+ {
+ CurrentBufferObjectWidth--;
+ }
+
+ return --CurrentBufferIndex >= 0;
+ }
+
+ private void DecodeAreaData()
+ {
+ // Render object in buffer if we have one.
+ if (CurrentBufferEnabled)
+ {
+ AreaDataIndex = CurrentBufferObjectIndex;
+ }
+
+ // Do not render if end of area or if we had a screen jump command.
+ if (IsEndOfArea || IsScreenJumpCommand)
+ {
+ return;
+ }
+
+ // Do not render object at wrong coordinate.
+ if (!IsObjectAtRenderCoordinate())
+ {
+ return;
+ }
+
+ // Get the render command for the specific object.
+ if (RenderCommands.TryGetValue(CurrentObjectCode, out var command))
+ {
+ command();
+ }
+ }
+
+ private bool IsObjectAtRenderCoordinate()
+ {
+ // If we're rendering a buffer object, then it is guaranteed that it is at the
+ // rendering coordinate.
+ if (CurrentBufferEnabled)
+ {
+ return true;
+ }
+
+ // Do not render objects on wrong screen.
+ if (CurrentObjectScreen != CurrentRenderingScreen)
+ {
+ return false;
+ }
+
+ // Render object if it is on the rendering X-coordinate.
+ if (CurrentObjectCommand.X == CurrentRenderingScreenX)
+ {
+ // Save this object to the current buffer.
+ CurrentBufferObjectIndex = AreaDataIndex;
+
+ // We can now start reading at the next object.
+ IncrementAreaDataIndex();
+ return true;
+ }
+
+ return false;
+ }
+
+ private void InitSingleTileRow(int tile)
+ {
+ _ = TrySetCurrentBufferObjectWidth();
+ RenderSingleTile(tile);
+ }
+
+ private void RenderSingleTile(int tile)
+ {
+ RenderTileColumn(tile, CurrentObjectCommand.Y, 0);
+ }
+
+ private void RenderTileColumn(int tile)
+ {
+ (var y, var height) = GetObjectColumnProperties();
+ RenderTileColumn(tile, y, height);
+ }
+
+ private void RenderTileColumn(int tile, int y, int extraHeight)
+ {
+ for (; extraHeight-- >= 0 && y < 0x0D; y++)
+ {
+ if (CanWriteTile(tile, TileBuffer[y]))
+ {
+ TileBuffer[y] = tile;
+ }
+ }
+ }
+
+ private bool TrySetCurrentBufferObjectWidth()
+ {
+ return TrySetCurrentBufferObjectWidth(
+ CurrentBufferObject.Parameter);
+ }
+
+ private bool TrySetCurrentBufferObjectWidth(int width)
+ {
+ if (CurrentBufferEnabled)
+ {
+ return false;
+ }
+
+ CurrentBufferObjectWidth = width;
+ return true;
+ }
+
+ private (int y, int parameter) GetObjectColumnProperties()
+ {
+ return (
+ CurrentBufferObject.Y,
+ CurrentBufferObject.Parameter);
+ }
+
+ private void Pipe()
+ {
+ _ = TrySetCurrentBufferObjectWidth(1);
+ (var y, var height) = GetObjectColumnProperties();
+ height &= 7;
+
+ var index = CurrentBufferObjectWidth;
+ if (CurrentObjectCode == AreaObjectCode.UnenterablePipe)
+ {
+ index += 4;
+ }
+
+ if (height == 0)
+ {
+ height++;
+ }
+
+ TileBuffer[y] = PipeTileTable[index];
+ RenderTileColumn(
+ PipeTileTable[index + 2],
+ y + 1,
+ height - 1);
+ }
+
+ private void AreaSpecificPlatform()
+ {
+ switch (CurrentHeader.AreaPlatformType)
+ {
+ case AreaPlatformType.Trees:
+ RenderTreePlatform();
+ break;
+
+ case AreaPlatformType.Mushrooms:
+ RenderMushroomPlatform();
+ break;
+
+ case AreaPlatformType.CloudGround:
+ RenderCloudPlatform();
+ break;
+
+ case AreaPlatformType.BulletBillTurrets:
+ RenderBulletBillTurrets();
+ break;
+ }
+ }
+
+ private void RenderTreePlatform()
+ {
+ var (y, width) = GetObjectColumnProperties();
+ if (CurrentBufferObjectWidth == 0)
+ {
+ RenderSingleTile(0x1C);
+ }
+ else if (CurrentBufferObjectWidth > 0)
+ {
+ RenderTreePlatformColumn(y, CurrentBufferObjectWidth);
+ }
+ else
+ {
+ CurrentBufferObjectWidth = width;
+ if (CurrentRenderingX == 0)
+ {
+ RenderTreePlatformColumn(y, width);
+ }
+ else
+ {
+ RenderTileColumn(0x1A, y, 0);
+ }
+ }
+ }
+
+ private void RenderTreePlatformColumn(int y, int width)
+ {
+ TileBuffer[y++] = 0x1B;
+ if (--width == 0)
+ {
+ if (ObjectHasSpecialProperties)
+ {
+ ObjectHasSpecialProperties = false;
+ TileBuffer[y] = 0x47;
+ RenderStemColumn(0x4B, y);
+ }
+ else
+ {
+ ObjectHasSpecialProperties = false;
+ TileBuffer[y] = 0x48;
+ RenderStemColumn(0x4C, y);
+ }
+ }
+ else
+ {
+ if (ObjectHasSpecialProperties)
+ {
+ TileBuffer[y] = 0x46;
+ RenderStemColumn(0x4A, y);
+ }
+ else
+ {
+ ObjectHasSpecialProperties = true;
+ TileBuffer[y] = 0x45;
+ RenderStemColumn(0x49, y);
+ }
+ }
+ }
+
+ private void RenderStemColumn(int tile, int y)
+ {
+ RenderTileColumn(tile, y + 1, 0x0F);
+ }
+
+ private void RenderMushroomPlatform()
+ {
+ (var y, var width) = GetObjectColumnProperties();
+ if (TrySetCurrentBufferObjectWidth(width))
+ {
+ ObjectSpecialCoordinate =
+ CurrentBufferObjectWidth >> 1;
+
+ RenderSingleTile(0x1D);
+ }
+ else
+ {
+ if (CurrentBufferObjectWidth != 0)
+ {
+ RenderSingleTile(0x1E);
+ if (CurrentBufferObjectWidth == ObjectSpecialCoordinate)
+ {
+ TileBuffer[++y] = 0x4F;
+ RenderStemColumn(0x50, y);
+ }
+ }
+ else
+ {
+ RenderSingleTile(0x1F);
+ }
+ }
+ }
+
+ private void RenderCloudPlatform()
+ {
+ // Unverified
+ RenderTreePlatform();
+ }
+
+ private void RenderBulletBillTurrets()
+ {
+ (var y, var height) = GetObjectColumnProperties();
+ TileBuffer[y++] = 0x6C;
+ if (--height >= 0)
+ {
+ TileBuffer[y++] = 0x6D;
+ RenderTileColumn(0x6E, y, --height);
+ }
+ }
+
+ private void RowOfBricks()
+ {
+ var index = AreaObjectRenderer.IsCloudPlatform
+ ? 4
+ : (int)AreaType;
+
+ var tile = BrickRowTileTable[index];
+ InitSingleTileRow(tile);
+ }
+
+ private void RowOfStones()
+ {
+ var tile = StoneRowTileTable[(int)AreaType];
+
+ InitSingleTileRow(tile);
+ }
+
+ private void RowOfCoins()
+ {
+ var tile = CoinRowTileTable[(int)AreaType];
+
+ InitSingleTileRow(tile);
+ }
+
+ private void ColumnOfBricks()
+ {
+ var tile = BrickRowTileTable[(int)AreaType];
+
+ RenderTileColumn(tile);
+ }
+
+ private void ColumnOfStones()
+ {
+ var tile = StoneRowTileTable[(int)AreaType];
+
+ RenderTileColumn(tile);
+ }
+
+ private void Hole()
+ {
+ _ = TrySetCurrentBufferObjectWidth();
+ RenderTileColumn(0, 0x08, 0x0F);
+ }
+
+ private void BalanceHorizontalRope()
+ {
+ var index = TrySetCurrentBufferObjectWidth()
+ ? 0
+ : CurrentBufferObjectWidth != 0
+ ? 1
+ : 2;
+
+ var tile = PulleyRopeTileTable[index];
+ TileBuffer[0] = tile;
+ }
+
+ private void HighBridge()
+ {
+ Bridge(6);
+ }
+
+ private void MidBridge()
+ {
+ Bridge(7);
+ }
+
+ private void LowBridge()
+ {
+ Bridge(9);
+ }
+
+ private void Bridge(int y)
+ {
+ _ = TrySetCurrentBufferObjectWidth();
+ if (CurrentBufferObjectWidth != 0)
+ {
+ if (!ObjectHasSpecialProperties)
+ {
+ ObjectHasSpecialProperties = true;
+ RenderBridge(0x0E);
+ }
+ else
+ {
+ RenderBridge(0x0D);
+ }
+ }
+ else
+ {
+ ObjectHasSpecialProperties = false;
+ RenderBridge(0x0F);
+ }
+
+ void RenderBridge(int tile)
+ {
+ TileBuffer[y++] = tile;
+ RenderTileColumn(0x6B, y, 0);
+ }
+ }
+
+ private void HoleWithWaterOrLava()
+ {
+ _ = TrySetCurrentBufferObjectWidth();
+ var y = AreaType == AreaType.Castle ? 0x0B : 0x0A;
+ var tile = WaterSurfaceTileTable[(int)AreaType];
+ TileBuffer[y++] = tile;
+
+ tile = WaterSurfaceTileTable[4 + ((int)AreaType >> 1)];
+ RenderTileColumn(tile, y, 1);
+ }
+
+ private void HighRowOfCoinBlocks()
+ {
+ RowOfCoinBlocks(3);
+ }
+
+ private void LowRowOfCoinBlocks()
+ {
+ RowOfCoinBlocks(7);
+ }
+
+ private void RowOfCoinBlocks(int y)
+ {
+ _ = TrySetCurrentBufferObjectWidth();
+ TileBuffer[y] = 0xE7;
+ }
+
+ private void ItemBlock()
+ {
+ var tile = SingleTileObjectTable[CurrentObjectCommand.Parameter];
+ TileBuffer[CurrentBufferObject.Y] = tile;
+ }
+
+ private void AreaTypeBlock()
+ {
+ var index = CurrentBufferObject.Parameter;
+ if (AreaType == AreaType.Water)
+ {
+ index += 5;
+ }
+
+ var tile = SingleTileObjectTable[index];
+ TileBuffer[CurrentBufferObject.Y] = tile;
+ }
+
+ private void SidewaysPipe()
+ {
+ var y = CurrentBufferObject.Y;
+ TileBuffer[y++] = 0x75;
+ TileBuffer[y] = 0x76;
+ }
+
+ private void FireBarBlock()
+ {
+ RenderSingleTile(0xFC);
+ }
+
+ private void SpringBoard()
+ {
+ var y = CurrentBufferObject.Y;
+ TileBuffer[y++] = 0x6F;
+ TileBuffer[y] = 0x70;
+ }
+
+ private void JPipe()
+ {
+ var drawVertical = !TrySetCurrentBufferObjectWidth(3);
+ var index = CurrentBufferObjectWidth;
+ var tile = JPipeTilesTable1[index];
+ if (tile != 0)
+ {
+ RenderTileColumn(tile, 0, 8);
+ }
+ else
+ {
+ drawVertical = false;
+ }
+
+ TileBuffer[9] = JPipeTilesTable2[index];
+ TileBuffer[10] = JPipeTilesTable3[index];
+ if (!drawVertical)
+ {
+ return;
+ }
+
+ Array.Clear(TileBuffer, 0, 7);
+ TileBuffer[7] = JPipeTilesTable4[index];
+ }
+
+ private void FlagPole()
+ {
+ TileBuffer[0] = 0x28;
+ RenderTileColumn(0x29, 1, 8);
+ TileBuffer[0x0A] = 0x64;
+ }
+
+ private void BowserAxe()
+ {
+ BowserBridge(0xFD, 6);
+ }
+
+ private void RopeForAxe()
+ {
+ BowserBridge(0x10, 7);
+ }
+
+ private void BowserBridge()
+ {
+ _ = TrySetCurrentBufferObjectWidth(0x0C);
+ BowserBridge(0x8D, 8);
+ }
+
+ private void BowserBridge(int tile, int y)
+ {
+ RenderTileColumn(tile, y, 0);
+ }
+
+ private void ForegroundChange()
+ {
+ var header = CurrentHeader;
+ header.ForegroundScenery = (CurrentBufferObject.BaseCommand & 7) < 4
+ ? (ForegroundScenery)(CurrentBufferObject.BaseCommand & 7)
+ : ForegroundScenery.None;
+ CurrentHeader = header;
+ }
+
+ private void TerrainModifier()
+ {
+ var header = CurrentHeader;
+ header.TerrainMode = (TerrainMode)
+ (CurrentBufferObject.BaseCommand & 0x0F);
+
+ header.BackgroundScenery = (BackgroundScenery)
+ ((CurrentBufferObject.BaseCommand & 0x30) >> 4);
+
+ CurrentHeader = header;
+ }
+
+ private void RopeForLift()
+ {
+ RenderTileColumn(0x40, 0, 0x0F);
+ }
+
+ private void PulleyRope()
+ {
+ RenderTileColumn(0x44, 1, 0x0F);
+ (_, var height) = GetObjectColumnProperties();
+
+ RenderTileColumn(0x40, 1, height);
+ }
+
+ private void Castle()
+ {
+ var parameter = CurrentBufferObject.Parameter;
+ var y = parameter;
+ _ = TrySetCurrentBufferObjectWidth(y == 0 ? 9 : y + 1);
+ var index = CurrentBufferObjectWidth;
+ for (var i = 0x16; y != 0x0B;)
+ {
+ // Bounds check not in game.
+ if (index >= CastleTileTable.Length || y >= TileBuffer.Length)
+ {
+ break;
+ }
+
+ var tile = CastleTileTable[index];
+ TileBuffer[y++] = tile;
+ if (i != 0)
+ {
+ index += 0x0A;
+ i--;
+ }
+ }
+
+ if (CurrentBufferObject.Parameter != 0
+ && CurrentBufferObjectWidth == 0)
+ {
+ TileBuffer[0x0A] = 0;
+ }
+
+ if (CurrentRenderingScreen != 0 && TileBuffer[0x0B] == 0x56)
+ {
+ TileBuffer[0x0B] = 0xFB;
+ }
+ }
+
+ private void CastleCeilingCap()
+ {
+ (var y, _) = GetObjectColumnProperties();
+ _ = TrySetCurrentBufferObjectWidth();
+ var firstTile = TileBuffer[0];
+ var secondTile = firstTile + (firstTile == 0x65 ? +1 : -1);
+ do
+ {
+ // Check not in the game.
+ if (y >= TileBuffer.Length)
+ {
+ break;
+ }
+
+ TileBuffer[y] = (y & 1) == 0 ? firstTile : secondTile;
+ y++;
+ }
+ while (--CurrentBufferObjectWidth >= 0);
+ }
+
+ private void StoneStairs()
+ {
+ (_, var height) = GetObjectColumnProperties();
+ if (TrySetCurrentBufferObjectWidth(height))
+ {
+ ObjectParameter = 9;
+ }
+
+ // Bounds check not in game.
+ if (ObjectParameter == 0)
+ {
+ return;
+ }
+
+ int y = StoneStairYTable[--ObjectParameter];
+ height = StoneStairHeightTable[ObjectParameter];
+ RenderTileColumn(0x64, y, height);
+ }
+
+ private void CastleDescendingSteps()
+ {
+ _ = TrySetCurrentBufferObjectWidth();
+ (var y, _) = GetObjectColumnProperties();
+ if (CurrentBufferObjectWidth == 0)
+ {
+ TileBuffer[y++] = 0xF3;
+ if (TileBuffer[y] == 0)
+ {
+ TileBuffer[y++] = 0xF4;
+ }
+ }
+ else if (TileBuffer[y] == 0)
+ {
+ TileBuffer[y] = 0xF5;
+ }
+
+ do
+ {
+ if (TileBuffer[y] == 0)
+ {
+ TileBuffer[y] = 0xF6;
+ }
+ }
+ while (++y != 0x0D);
+ }
+
+ private void CastleRectangularCeilingTiles()
+ {
+ (var y, _) = GetObjectColumnProperties();
+ _ = TrySetCurrentBufferObjectWidth();
+ TileBuffer[y] = 0x67;
+ for (y += 2; y < TileBuffer.Length; y += 2)
+ {
+ if (TileBuffer[y] is not 0x65 and not 0x66)
+ {
+ break;
+ }
+
+ TileBuffer[y] = 0x67;
+ }
+ }
+
+ private void CastleFloorRightEdge()
+ {
+ _ = TrySetCurrentBufferObjectWidth();
+ (var y, _) = GetObjectColumnProperties();
+
+ // Game engine does not check y bounds here.
+ if (y < TileBuffer.Length)
+ {
+ TileBuffer[y++] = 0xF7;
+ while (y != TileBuffer.Length && TileBuffer[y] != 0xEB)
+ {
+ TileBuffer[y++] = 0xF8;
+ }
+ }
+ }
+
+ private void CastleFloorLeftEdge()
+ {
+ _ = TrySetCurrentBufferObjectWidth();
+ (var y, _) = GetObjectColumnProperties();
+ if (y >= TileBuffer.Length)
+ {
+ return;
+ }
+
+ if (TileBuffer[y] != 0xFC)
+ {
+ TileBuffer[y] = 0xF9;
+ }
+
+ while (++y != 0x0D && TileBuffer[y] != 0xF0)
+ {
+ TileBuffer[y] = 0xFA;
+ }
+ }
+
+ private void CastleFloorLeftWall()
+ {
+ _ = TrySetCurrentBufferObjectWidth();
+ (var y, _) = GetObjectColumnProperties();
+ if (CurrentBufferObjectWidth == 0)
+ {
+ TileBuffer[y++] = 2;
+ TileBuffer[y] = 0xED;
+ }
+ else
+ {
+ TileBuffer[y] = TileBuffer[y] == 0x68 ? 0xEE : 0xEB;
+
+ // Game engine does not check y bounds here.
+ if (++y < TileBuffer.Length)
+ {
+ TileBuffer[y] = 0xEC;
+
+ // Game engine does not check y bounds here.
+ if (++y < TileBuffer.Length)
+ {
+ TileBuffer[y] = 0x69;
+ }
+ }
+ }
+ }
+
+ private void CastleFloorRightWall()
+ {
+ (var y, _) = GetObjectColumnProperties();
+ _ = TrySetCurrentBufferObjectWidth();
+ if (CurrentBufferObjectWidth == 0)
+ {
+ TileBuffer[y] = TileBuffer[y] == 0x68 ? 0xF2 : 0xF0;
+ TileBuffer[++y] = 0xF1;
+ while (++y != 0x0D)
+ {
+ TileBuffer[y] = 0x69;
+ }
+ }
+ else
+ {
+ TileBuffer[y] = 3;
+ TileBuffer[y + 1] = 0xEF;
+ }
+ }
+
+ private void VerticalSeaBlocks()
+ {
+ _ = TrySetCurrentBufferObjectWidth();
+ (var y, _) = GetObjectColumnProperties();
+ do
+ {
+ TileBuffer[y++] = 0x71;
+ }
+ while (--CurrentBufferObjectWidth >= 0);
+ }
+
+ private void ExtendableJPipe()
+ {
+ _ = TrySetCurrentBufferObjectWidth(3);
+ (_, var height) = GetObjectColumnProperties();
+ height -= 2;
+ var y = height + 1;
+ var index = CurrentBufferObjectWidth;
+ var tile = JPipeTilesTable5[index];
+ if (tile != 0)
+ {
+ RenderTileColumn(tile, 0, height);
+ }
+
+ // Bounds checks not in the game.
+ if (y >= 0 && y < TileBuffer.Length)
+ {
+ tile = JPipeTilesTable6[index];
+ TileBuffer[y] = tile;
+ }
+
+ if (++y >= 0 && y < TileBuffer.Length)
+ {
+ tile = JPipeTilesTable7[index];
+ TileBuffer[y] = tile;
+ }
+ }
+
+ private void VerticalClimbingObject()
+ {
+ (var _, var height) = GetObjectColumnProperties();
+ RenderTileColumn(0x77, 2, height);
+ }
+}
diff --git a/src/Smas/Smb1/AreaData/ObjectData/AreaObjectRenderer.cs b/src/Smas/Smb1/AreaData/ObjectData/AreaObjectRenderer.cs
new file mode 100644
index 0000000..fb5ebe4
--- /dev/null
+++ b/src/Smas/Smb1/AreaData/ObjectData/AreaObjectRenderer.cs
@@ -0,0 +1,565 @@
+//
+// Copyright (c) 2022 spel werdz rite. All rights reserved. Licensed under GNU Affero
+// General Public License. See LICENSE in project root for full license
+// information, or visit https://www.gnu.org/licenses/#AGPL
+//
+
+namespace Maseya.Smas.Smb1.AreaData.ObjectData;
+
+using System;
+using System.Collections.Generic;
+
+using HeaderData;
+
+using Snes;
+
+public class AreaObjectRenderer
+{
+ ///
+ /// The height of the tile buffer. This field is constant.
+ ///
+ public const int TileBufferSize = 0x0D;
+
+ private const int BackgroundSceneryMetaDataOffsetTableSize = 3;
+ private const int BackgroundSceneryMetaDataTableSize = 0x90;
+ private const int BackgroundSceneryTileDataTableSize = 0x24;
+
+ private const int ForegroundSceneryDataOffsetTableSize = 3;
+ private const int ForegroundSceneryDataTableSize = 0x27;
+
+ private const int TerrainAreaTypeTableSize = 4;
+ private const int TerrainBitMaskTableSize = 0x20;
+
+ private const int BitmaskTableSize = 8;
+
+ private const int PulleyRopeTileTableSize = 3;
+ private const int JPipeTilesTable1Size = 4;
+ private const int JPipeTilesTable2Size = 4;
+ private const int JPipeTilesTable3Size = 4;
+ private const int JPipeTilesTable4Size = 8;
+ private const int PipeTileTableSize = 8;
+ private const int WaterSurfaceTileTableSize = 6;
+ private const int CoinRowTileTableSize = 4;
+ private const int BrickRowTileTableSize = 5;
+ private const int StoneRowTileTableSize = 4;
+ private const int SingleTileObjectTableSize = 0x0E;
+ private const int CastleTileTableSize = 0x6E;
+ private const int StoneStairYTableSize = 9;
+ private const int StoneStairHeightTableSize = 9;
+ private const int JPipeTilesTable5Size = 4;
+ private const int JPipeTilesTable6Size = 4;
+ private const int JPipeTilesTable7Size = 4;
+
+ public AreaObjectRenderer(Rom rom, AreaObjectRendererPointers pointers)
+ {
+ BackgroundSceneryMetaDataOffsetTable = rom.ReadBytesIndirectIndexed(
+ pointers.BackgroundSceneryMetaDataOffsetTablePointer,
+ index: 1,
+ BackgroundSceneryMetaDataOffsetTableSize);
+ BackgroundSceneryMetaDataTable = rom.ReadBytesIndirect(
+ pointers.BackgroundSceneryMetaDataTablePointer,
+ BackgroundSceneryMetaDataTableSize);
+ BackgroundSceneryTileDataTable = rom.ReadBytesIndirect(
+ pointers.BackgroundSceneryTileDataTablePointer,
+ BackgroundSceneryTileDataTableSize);
+ ForegroundSceneryDataOffsetTable = rom.ReadBytesIndirectIndexed(
+ pointers.ForegroundSceneryDataOffsetTablePointer,
+ index: 1,
+ ForegroundSceneryDataOffsetTableSize);
+ ForegroundSceneryDataTable = rom.ReadBytesIndirect(
+ pointers.ForegroundSceneryDataTablePointer,
+ ForegroundSceneryDataTableSize);
+ TerrainAreaTypeTable = rom.ReadBytesIndirect(
+ pointers.TerrainAreaTypeTablePointer,
+ TerrainAreaTypeTableSize);
+ TerrainBitMaskTable = rom.ReadBytesIndirect(
+ pointers.TerrainBitMaskTablePointer,
+ TerrainBitMaskTableSize);
+ BitmaskTable = rom.ReadBytesIndirect(
+ pointers.BitmaskTablePointer,
+ BitmaskTableSize);
+
+ PulleyRopeTileTable = rom.ReadBytesIndirect(
+ pointers.PulleyRopeTileTablePointer,
+ PulleyRopeTileTableSize);
+ JPipeTilesTable1 = rom.ReadBytesIndirect(
+ pointers.JPipeTilesTable1Pointer,
+ JPipeTilesTable1Size);
+ JPipeTilesTable2 = rom.ReadBytesIndirect(
+ pointers.JPipeTilesTable2Pointer,
+ JPipeTilesTable2Size);
+ JPipeTilesTable3 = rom.ReadBytesIndirect(
+ pointers.JPipeTilesTable3Pointer,
+ JPipeTilesTable3Size);
+ JPipeTilesTable4 = rom.ReadBytesIndirect(
+ pointers.JPipeTilesTable4Pointer,
+ JPipeTilesTable4Size);
+ PipeTileTable = rom.ReadBytesIndirect(
+ pointers.PipeTileTablePointer,
+ PipeTileTableSize);
+ WaterSurfaceTileTable = rom.ReadBytesIndirect(
+ pointers.WaterSurfaceTileTablePointer,
+ WaterSurfaceTileTableSize);
+ CoinRowTileTable = rom.ReadBytesIndirect(
+ pointers.CoinRowTileTablePointer,
+ CoinRowTileTableSize);
+ BrickRowTileTable = rom.ReadBytesIndirect(
+ pointers.BrickRowTileTablePointer,
+ BrickRowTileTableSize);
+ StoneRowTileTable = rom.ReadBytesIndirect(
+ pointers.StoneRowTileTablePointer,
+ StoneRowTileTableSize);
+ SingleTileObjectTable = rom.ReadBytesIndirect(
+ pointers.SingleTileObjectTablePointer,
+ SingleTileObjectTableSize);
+ CastleTileTable = rom.ReadBytesIndirect(
+ pointers.CastleTileTablePointer,
+ CastleTileTableSize);
+ StoneStairYTable = rom.ReadBytesIndirect(
+ pointers.StoneStairYTablePointer,
+ StoneStairYTableSize);
+ StoneStairHeightTable = rom.ReadBytesIndirect(
+ pointers.StoneStairHeightTablePointer,
+ StoneStairHeightTableSize);
+ JPipeTilesTable5 = rom.ReadBytesIndirect(
+ pointers.JPipeTilesTable5Pointer,
+ JPipeTilesTable5Size);
+ JPipeTilesTable6 = rom.ReadBytesIndirect(
+ pointers.JPipeTilesTable6Pointer,
+ JPipeTilesTable6Size);
+ JPipeTilesTable7 = rom.ReadBytesIndirect(
+ pointers.JPipeTilesTable7Pointer,
+ JPipeTilesTable7Size);
+
+ TileBuffer = new int[TileBufferSize];
+ }
+
+ public byte[] BackgroundSceneryMetaDataOffsetTable { get; }
+
+ public byte[] BackgroundSceneryMetaDataTable { get; }
+
+ public byte[] BackgroundSceneryTileDataTable { get; }
+
+ public byte[] ForegroundSceneryDataOffsetTable { get; }
+
+ public byte[] ForegroundSceneryDataTable { get; }
+
+ public byte[] TerrainAreaTypeTable { get; }
+
+ public byte[] TerrainBitMaskTable { get; }
+
+ public byte[] BitmaskTable { get; }
+
+ public byte[] PulleyRopeTileTable { get; }
+
+ public byte[] JPipeTilesTable1 { get; }
+
+ public byte[] JPipeTilesTable2 { get; }
+
+ public byte[] JPipeTilesTable3 { get; }
+
+ public byte[] JPipeTilesTable4 { get; }
+
+ public byte[] PipeTileTable { get; }
+
+ public byte[] WaterSurfaceTileTable { get; }
+
+ public byte[] CoinRowTileTable { get; }
+
+ public byte[] BrickRowTileTable { get; }
+
+ public byte[] StoneRowTileTable { get; }
+
+ public byte[] SingleTileObjectTable { get; }
+
+ public byte[] CastleTileTable { get; }
+
+ public byte[] StoneStairYTable { get; }
+
+ public byte[] StoneStairHeightTable { get; }
+
+ public byte[] JPipeTilesTable5 { get; }
+
+ public byte[] JPipeTilesTable6 { get; }
+
+ public byte[] JPipeTilesTable7 { get; }
+
+ public AreaType AreaType
+ {
+ get;
+ private set;
+ }
+
+ public AreaHeader CurrentHeader
+ {
+ get;
+ set;
+ }
+
+ public bool IsCloudPlatform
+ {
+ get
+ {
+ return CurrentHeader.AreaPlatformType == AreaPlatformType.CloudGround;
+ }
+ }
+
+ public int[] TileBuffer
+ {
+ get;
+ }
+
+ ///
+ /// Gets or sets $0725, which represents the screen the renderer is currently on.
+ ///
+ public int CurrentRenderingScreenX
+ {
+ get;
+ set;
+ }
+
+ ///
+ /// Gets or sets $0726, which represents the X-coordinate of the current screen the
+ /// renderer is currently on.
+ ///
+ public int CurrentRenderingScreen
+ {
+ get;
+ set;
+ }
+
+ ///
+ /// Gets or sets the full X-coordinate the renderer is currently on.
+ ///
+ public int CurrentRenderingX
+ {
+ get
+ {
+ return (CurrentRenderingScreen * 0x10) + CurrentRenderingScreenX;
+ }
+
+ set
+ {
+ CurrentRenderingScreen = value >> 4;
+ CurrentRenderingScreenX = value & 0x0F;
+ }
+ }
+
+ public void WriteToGameData(
+ Rom rom,
+ AreaObjectRendererPointers pointers)
+ {
+ rom.WriteBytesIndirect(
+ pointers.JPipeTilesTable7Pointer,
+ JPipeTilesTable7);
+ rom.WriteBytesIndirect(
+ pointers.JPipeTilesTable6Pointer,
+ JPipeTilesTable6);
+ rom.WriteBytesIndirect(
+ pointers.JPipeTilesTable5Pointer,
+ JPipeTilesTable5);
+ rom.WriteBytesIndirect(
+ pointers.StoneStairHeightTablePointer,
+ StoneStairHeightTable);
+ rom.WriteBytesIndirect(
+ pointers.StoneStairYTablePointer,
+ StoneStairYTable);
+ rom.WriteBytesIndirect(
+ pointers.CastleTileTablePointer,
+ CastleTileTable);
+ rom.WriteBytesIndirect(
+ pointers.SingleTileObjectTablePointer,
+ SingleTileObjectTable);
+ rom.WriteBytesIndirect(
+ pointers.StoneRowTileTablePointer,
+ StoneRowTileTable);
+ rom.WriteBytesIndirect(
+ pointers.BrickRowTileTablePointer,
+ BrickRowTileTable);
+ rom.WriteBytesIndirect(
+ pointers.CoinRowTileTablePointer,
+ CoinRowTileTable);
+ rom.WriteBytesIndirect(
+ pointers.WaterSurfaceTileTablePointer,
+ WaterSurfaceTileTable);
+ rom.WriteBytesIndirect(
+ pointers.PipeTileTablePointer,
+ PipeTileTable);
+ rom.WriteBytesIndirect(
+ pointers.JPipeTilesTable4Pointer,
+ JPipeTilesTable4);
+ rom.WriteBytesIndirect(
+ pointers.JPipeTilesTable3Pointer,
+ JPipeTilesTable3);
+ rom.WriteBytesIndirect(
+ pointers.JPipeTilesTable2Pointer,
+ JPipeTilesTable2);
+ rom.ReadBytesIndirect(
+ pointers.JPipeTilesTable1Pointer,
+ JPipeTilesTable1);
+ rom.WriteBytesIndirect(
+ pointers.PulleyRopeTileTablePointer,
+ PulleyRopeTileTable);
+
+ rom.WriteBytesIndirect(
+ pointers.BitmaskTablePointer,
+ BitmaskTable);
+ rom.WriteBytesIndirect(
+ pointers.TerrainBitMaskTablePointer,
+ TerrainBitMaskTable);
+ rom.WriteBytesIndirect(
+ pointers.TerrainAreaTypeTablePointer,
+ TerrainAreaTypeTable);
+ rom.WriteBytesIndirect(
+ pointers.ForegroundSceneryDataTablePointer,
+ ForegroundSceneryDataTable);
+ rom.WriteBytesIndirectIndexed(
+ pointers.ForegroundSceneryDataOffsetTablePointer,
+ 1,
+ ForegroundSceneryDataOffsetTable);
+ rom.WriteBytesIndirect(
+ pointers.BackgroundSceneryTileDataTablePointer,
+ BackgroundSceneryTileDataTable);
+ rom.WriteBytesIndirect(
+ pointers.BackgroundSceneryMetaDataTablePointer,
+ BackgroundSceneryMetaDataTable);
+ rom.WriteBytesIndirectIndexed(
+ pointers.BackgroundSceneryMetaDataOffsetTablePointer,
+ 1,
+ BackgroundSceneryMetaDataOffsetTable);
+ }
+
+ public void RenderTileMap(
+ Span tilemap,
+ AreaType areaType,
+ AreaHeader header,
+ IList areaObjectData,
+ bool isUnderwaterCastle)
+ {
+ CurrentHeader = header;
+ AreaType = areaType;
+ var areaObjectParser = new AreaObjectParser(
+ this,
+ areaObjectData);
+
+ tilemap.Clear();
+
+ areaObjectParser.ResetBuffer();
+ for (CurrentRenderingScreen = 0; CurrentRenderingScreen < 0x20; CurrentRenderingScreen++)
+ {
+ for (CurrentRenderingScreenX = 0; CurrentRenderingScreenX < 0x10; CurrentRenderingScreenX++)
+ {
+ ResetTileBuffer();
+ RenderBackground();
+ RenderForeground();
+ RenderTerrain(isUnderwaterCastle);
+ areaObjectParser.ParseAreaData();
+ WriteBufferToTileMap(tilemap);
+ }
+ }
+ }
+
+ private void ResetTileBuffer()
+ {
+ for (var i = 0; i < TileBuffer.Length; i++)
+ {
+ TileBuffer[i] = 0;
+ }
+ }
+
+ private void WriteBufferToTileMap(Span tilemap)
+ {
+ var startIndex = (2 * 0x20 * 0x10) + CurrentRenderingX;
+ for (var i = 0; i < TileBuffer.Length; i++)
+ {
+ tilemap[startIndex + (i * 0x20 * 0x10)] = TileBuffer[i];
+ }
+
+ if (TileBuffer[0] == 0x40)
+ {
+ tilemap[startIndex + (-1 * 0x20 * 0x10)] = 0x40;
+ tilemap[startIndex + (-2 * 0x20 * 0x10)] = 0x40;
+ }
+ }
+
+ private void RenderBackground()
+ {
+ if (CurrentHeader.BackgroundScenery == BackgroundScenery.None)
+ {
+ return;
+ }
+
+ var sceneryTile = GetBackgroundSceneryMetaTile(
+ CurrentHeader.BackgroundScenery,
+ CurrentRenderingScreen,
+ CurrentRenderingScreenX);
+
+ if (sceneryTile == 0)
+ {
+ return;
+ }
+
+ var startIndex = ((sceneryTile & 0x0F) - 1) * 3;
+ var y = sceneryTile >> 4;
+ for (var i = 0; i < 3; i++)
+ {
+ var tile = BackgroundSceneryTileDataTable[startIndex + i];
+ TileBuffer[y + i] = tile;
+
+ if (y + i + 1 == 0x0B)
+ {
+ break;
+ }
+ }
+ }
+
+ private void RenderForeground()
+ {
+ if (CurrentHeader.ForegroundScenery == ForegroundScenery.None)
+ {
+ return;
+ }
+
+ var index = GetForegroundSceneryIndex(CurrentHeader.ForegroundScenery);
+ var waterTileChange = false;
+ for (var y = 0; y < 0x0D; y++)
+ {
+ var tile = ForegroundSceneryDataTable[index + y];
+ if (tile == 0)
+ {
+ continue;
+ }
+
+ if (AreaType != AreaType.Water)
+ {
+ if (AreaType == AreaType.Castle && tile == 0x86)
+ {
+ tile += 2;
+ }
+ }
+ else
+ {
+ if (waterTileChange)
+ {
+ waterTileChange = false;
+ }
+ else
+ {
+ waterTileChange = true;
+ tile += 1 + 2;
+ }
+ }
+
+ TileBuffer[y] = tile;
+ }
+ }
+
+ private void RenderTerrain(bool isUnderwaterCastle)
+ {
+ var useOtherCastleTile = false;
+ var castleTile = (byte)0;
+ var castleLongTileOffset = (CurrentRenderingScreenX & 1) != 0;
+
+ // In the ROM, the actual check is for an underwater area in World
+ // 8. That's a dumb check and I'm not keeping track of the world number, so
+ // I'll do this check for the time being. See $03:A4D5 in the disassembly.
+ var terrainTile = isUnderwaterCastle
+ ? (byte)0x65
+ : IsCloudPlatform
+ ? (byte)0x8C
+ : TerrainAreaTypeTable[(int)AreaType];
+
+ var y = 0;
+ var terrainIndex = (int)CurrentHeader.TerrainMode << 1;
+
+ var areaType2 = isUnderwaterCastle ? AreaType.Castle : AreaType;
+
+ terrain_loop:
+ var terrainBits = TerrainBitMaskTable[terrainIndex];
+
+ terrainIndex++;
+ if (IsCloudPlatform && y != 0)
+ {
+ terrainBits &= 8;
+ }
+
+ var j = 0;
+ bit_loop:
+ var bitmask = BitmaskTable[j];
+ if ((terrainBits & bitmask) != 0)
+ {
+ if (areaType2 == AreaType.Castle && castleTile != 0)
+ {
+ terrainTile = 0x68;
+ }
+
+ TileBuffer[y] = terrainTile;
+ if (castleTile != 0 && areaType2 == AreaType.Castle)
+ {
+ castleTile++;
+ if (castleTile == 0)
+ {
+ TileBuffer[y]++;
+ terrainTile++;
+ }
+ }
+ else if (areaType2 == AreaType.Castle
+ && !useOtherCastleTile
+ && !castleLongTileOffset)
+ {
+ TileBuffer[y]++;
+ }
+ }
+ else
+ {
+ castleTile = 0xFE;
+ useOtherCastleTile = true;
+ }
+
+ if (++y == 0x0D)
+ {
+ goto end_loop;
+ }
+
+ if (AreaType == AreaType.Underground && y == 0x0B)
+ {
+ terrainTile = 0x56;
+ }
+
+ castleLongTileOffset ^= true;
+ if (++j != 8)
+ {
+ goto bit_loop;
+ }
+
+ if (terrainIndex != 0)
+ {
+ goto terrain_loop;
+ }
+
+ end_loop:
+ terrainTile = (byte)TileBuffer[0x0C];
+ if (terrainTile is 0x56 or 0x72)
+ {
+ TileBuffer[0x0C] = terrainTile + 1;
+ }
+ }
+
+ private int GetBackgroundSceneryMetaTile(
+ BackgroundScenery sceneryType,
+ int page,
+ int x)
+ {
+ var xIndex = ((page % 3) << 4) + x;
+
+ // Should never have scenery type 0.
+ var sceneryOffset = BackgroundSceneryMetaDataOffsetTable[
+ (int)sceneryType - 1];
+ return BackgroundSceneryMetaDataTable[sceneryOffset + xIndex];
+ }
+
+ private int GetForegroundSceneryIndex(ForegroundScenery sceneryType)
+ {
+ return ForegroundSceneryDataOffsetTable[(int)sceneryType - 1];
+ }
+}
diff --git a/src/Smas/Smb1/AreaData/ObjectData/AreaObjectRendererPointers.cs b/src/Smas/Smb1/AreaData/ObjectData/AreaObjectRendererPointers.cs
new file mode 100644
index 0000000..578e86e
--- /dev/null
+++ b/src/Smas/Smb1/AreaData/ObjectData/AreaObjectRendererPointers.cs
@@ -0,0 +1,200 @@
+//
+// Copyright (c) 2022 spel werdz rite. All rights reserved. Licensed under GNU Affero
+// General Public License. See LICENSE in project root for full license
+// information, or visit https://www.gnu.org/licenses/#AGPL
+//
+
+namespace Maseya.Smas.Smb1.AreaData.ObjectData;
+
+public class AreaObjectRendererPointers
+{
+ public static readonly AreaObjectRendererPointers Jp10 = new(
+ baseAddress1: 0x03A48A,
+ baseAddress2: 0x03A9B3,
+ baseAddress3: 0x048F71);
+
+ public static readonly AreaObjectRendererPointers Jp11 = new(
+ baseAddress1: 0x03A48A,
+ baseAddress2: 0x03A9B3,
+ baseAddress3: 0x048F71);
+
+ public static readonly AreaObjectRendererPointers Usa = new(
+ baseAddress1: 0x03A45E,
+ baseAddress2: 0x03A987,
+ baseAddress3: 0x048F61);
+
+ public static readonly AreaObjectRendererPointers UsaPlusW = new(
+ baseAddress1: 0x03A45E,
+ baseAddress2: 0x03A987,
+ baseAddress3: 0x048F61);
+
+ public static readonly AreaObjectRendererPointers Eu = new(
+ baseAddress1: 0x03A50B,
+ baseAddress2: 0x03AA34,
+ singleTileObjectTablePointer: 0x03C01A,
+ baseAddress3: 0x048F61);
+
+ public static readonly AreaObjectRendererPointers EuPlusW = new(
+ baseAddress1: 0x03A50B,
+ baseAddress2: 0x03AA34,
+ singleTileObjectTablePointer: 0x03C01A,
+ baseAddress3: 0x048F61);
+
+ public static readonly AreaObjectRendererPointers UsaSmb1 = new(
+ baseAddress1: 0x00A493,
+ baseAddress2: 0x00A9BC,
+ baseAddress3: 0x018F61);
+
+ public AreaObjectRendererPointers(
+ int backgroundSceneryMetaDataOffsetTablePointer,
+ int backgroundSceneryMetaDataTablePointer,
+ int backgroundSceneryTileDataTablePointer,
+ int foregroundSceneryDataOffsetTablePointer,
+ int foregroundSceneryDataTablePointer,
+ int terrainAreaTypeTablePointer,
+ int terrainBitMaskTablePointer,
+ int bitmaskTablePointer,
+ int pulleyRopeTileTablePointer,
+ int jPipeTiles1TablePointer,
+ int jPipeTiles2TablePointer,
+ int jPipeTiles3TablePointer,
+ int jPipeTiles4TablePointer,
+ int pipeTileTablePointer,
+ int waterSurfaceTileTablePointer,
+ int coinRowTileTablePointer,
+ int brickRowTileTablePointer,
+ int stoneRowTileTablePointer,
+ int singleTileObjectTablePointer,
+ int castleTileTablePointer,
+ int stoneStairYTablePointer,
+ int stoneStairHeightTablePointer,
+ int jPipeTiles5TablePointer,
+ int jPipeTiles6TablePointer,
+ int jPipeTiles7TablePointer)
+ {
+ BackgroundSceneryMetaDataOffsetTablePointer =
+ backgroundSceneryMetaDataOffsetTablePointer;
+ BackgroundSceneryMetaDataTablePointer =
+ backgroundSceneryMetaDataTablePointer;
+ BackgroundSceneryTileDataTablePointer =
+ backgroundSceneryTileDataTablePointer;
+ ForegroundSceneryDataOffsetTablePointer =
+ foregroundSceneryDataOffsetTablePointer;
+ ForegroundSceneryDataTablePointer = foregroundSceneryDataTablePointer;
+ TerrainAreaTypeTablePointer = terrainAreaTypeTablePointer;
+ TerrainBitMaskTablePointer = terrainBitMaskTablePointer;
+ BitmaskTablePointer = bitmaskTablePointer;
+
+ PulleyRopeTileTablePointer = pulleyRopeTileTablePointer;
+ JPipeTilesTable1Pointer = jPipeTiles1TablePointer;
+ JPipeTilesTable2Pointer = jPipeTiles2TablePointer;
+ JPipeTilesTable3Pointer = jPipeTiles3TablePointer;
+ JPipeTilesTable4Pointer = jPipeTiles4TablePointer;
+ PipeTileTablePointer = pipeTileTablePointer;
+ WaterSurfaceTileTablePointer = waterSurfaceTileTablePointer;
+ CoinRowTileTablePointer = coinRowTileTablePointer;
+ BrickRowTileTablePointer = brickRowTileTablePointer;
+ StoneRowTileTablePointer = stoneRowTileTablePointer;
+ SingleTileObjectTablePointer = singleTileObjectTablePointer;
+ CastleTileTablePointer = castleTileTablePointer;
+ StoneStairYTablePointer = stoneStairYTablePointer;
+ StoneStairHeightTablePointer = stoneStairHeightTablePointer;
+ JPipeTilesTable5Pointer = jPipeTiles5TablePointer;
+ JPipeTilesTable6Pointer = jPipeTiles6TablePointer;
+ JPipeTilesTable7Pointer = jPipeTiles7TablePointer;
+ }
+
+ private AreaObjectRendererPointers(
+ int baseAddress1,
+ int baseAddress2,
+ int baseAddress3)
+ : this(
+ baseAddress1,
+ baseAddress2,
+ singleTileObjectTablePointer: baseAddress2 + 0x2A4,
+ baseAddress3)
+ { }
+
+ private AreaObjectRendererPointers(
+ int baseAddress1,
+ int baseAddress2,
+ int singleTileObjectTablePointer,
+ int baseAddress3)
+ : this(
+ backgroundSceneryMetaDataOffsetTablePointer: baseAddress1,
+ backgroundSceneryMetaDataTablePointer: baseAddress1 + 0x07,
+ backgroundSceneryTileDataTablePointer: baseAddress1 + 0x22,
+ foregroundSceneryDataOffsetTablePointer: baseAddress1 + 0x37,
+ foregroundSceneryDataTablePointer: baseAddress1 + 0x3C,
+ terrainAreaTypeTablePointer: baseAddress1 + 0x87,
+ terrainBitMaskTablePointer: baseAddress1 + 0x9A,
+ bitmaskTablePointer: baseAddress1 + 0xB3,
+ pulleyRopeTileTablePointer: baseAddress2,
+ jPipeTiles1TablePointer: baseAddress2 + 0x2D,
+ jPipeTiles2TablePointer: baseAddress2 + 0x3E,
+ jPipeTiles3TablePointer: baseAddress2 + 0x44,
+ jPipeTiles4TablePointer: baseAddress2 + 0x56,
+ pipeTileTablePointer: baseAddress2 + 0xB7,
+ waterSurfaceTileTablePointer: baseAddress2 + 0x106,
+ coinRowTileTablePointer: baseAddress2 + 0x1A6,
+ brickRowTileTablePointer: baseAddress2 + 0x1E6,
+ stoneRowTileTablePointer: baseAddress2 + 0x1EE,
+ singleTileObjectTablePointer: singleTileObjectTablePointer,
+ castleTileTablePointer: baseAddress3,
+ stoneStairYTablePointer: baseAddress3 + 0xCF,
+ stoneStairHeightTablePointer: baseAddress3 + 0xD2,
+ jPipeTiles5TablePointer: baseAddress3 + 0x233,
+ jPipeTiles6TablePointer: baseAddress3 + 0x245,
+ jPipeTiles7TablePointer: baseAddress3 + 0x24B)
+ { }
+
+ public int BackgroundSceneryMetaDataOffsetTablePointer { get; }
+
+ public int BackgroundSceneryMetaDataTablePointer { get; }
+
+ public int BackgroundSceneryTileDataTablePointer { get; }
+
+ public int ForegroundSceneryDataOffsetTablePointer { get; }
+
+ public int ForegroundSceneryDataTablePointer { get; }
+
+ public int TerrainAreaTypeTablePointer { get; }
+
+ public int TerrainBitMaskTablePointer { get; }
+
+ public int BitmaskTablePointer { get; }
+
+ public int PulleyRopeTileTablePointer { get; }
+
+ public int JPipeTilesTable1Pointer { get; }
+
+ public int JPipeTilesTable2Pointer { get; }
+
+ public int JPipeTilesTable3Pointer { get; }
+
+ public int JPipeTilesTable4Pointer { get; }
+
+ public int PipeTileTablePointer { get; }
+
+ public int WaterSurfaceTileTablePointer { get; }
+
+ public int CoinRowTileTablePointer { get; }
+
+ public int BrickRowTileTablePointer { get; }
+
+ public int StoneRowTileTablePointer { get; }
+
+ public int SingleTileObjectTablePointer { get; }
+
+ public int CastleTileTablePointer { get; }
+
+ public int StoneStairYTablePointer { get; }
+
+ public int StoneStairHeightTablePointer { get; }
+
+ public int JPipeTilesTable5Pointer { get; }
+
+ public int JPipeTilesTable6Pointer { get; }
+
+ public int JPipeTilesTable7Pointer { get; }
+}
diff --git a/src/Smas/Smb1/AreaData/ObjectData/AreaPlatformType.cs b/src/Smas/Smb1/AreaData/ObjectData/AreaPlatformType.cs
new file mode 100644
index 0000000..559ce70
--- /dev/null
+++ b/src/Smas/Smb1/AreaData/ObjectData/AreaPlatformType.cs
@@ -0,0 +1,33 @@
+//
+// Copyright (c) 2022 spel werdz rite. All rights reserved. Licensed under GNU Affero
+// General Public License. See LICENSE in project root for full license
+// information, or visit https://www.gnu.org/licenses/#AGPL
+//
+
+namespace Maseya.Smas.Smb1.AreaData.ObjectData;
+
+///
+/// The platform type to use for the miscellaneous platform object.
+///
+public enum AreaPlatformType
+{
+ ///
+ /// Green tree platforms.
+ ///
+ Trees,
+
+ ///
+ /// Orange mushroom platforms.
+ ///
+ Mushrooms,
+
+ ///
+ /// Vertical bullet bill shooters.
+ ///
+ BulletBillTurrets,
+
+ ///
+ /// Cloud as ground tiles.
+ ///
+ CloudGround,
+}
diff --git a/src/Smas/Smb1/AreaData/SpriteData/AreaSpriteCode.cs b/src/Smas/Smb1/AreaData/SpriteData/AreaSpriteCode.cs
new file mode 100644
index 0000000..852662a
--- /dev/null
+++ b/src/Smas/Smb1/AreaData/SpriteData/AreaSpriteCode.cs
@@ -0,0 +1,254 @@
+//
+// Copyright (c) 2022 spel werdz rite. All rights reserved. Licensed under GNU Affero
+// General Public License. See LICENSE in project root for full license
+// information, or visit https://www.gnu.org/licenses/#AGPL
+//
+
+namespace Maseya.Smas.Smb1.AreaData.SpriteData;
+
+public enum AreaSpriteCode
+{
+ AreaPointer = -1,
+
+ ///
+ /// Green Koopa Troopa (walks off floors).
+ ///
+ GreenKoopaTroopa,
+
+ ///
+ /// Red Koopa Troopa (walks off floors).
+ ///
+ RedKoopaTroopa,
+
+ ///
+ /// Buzzy Beetle.
+ ///
+ BuzzyBeetle,
+
+ ///
+ /// Red Koopa Troopa (stays on floors).
+ ///
+ RedKoopaTroopa2,
+
+ ///
+ /// Green Koopa Troopa (walks in place).
+ ///
+ GreenKoopaTroopa2,
+
+ ///
+ /// Hammer Bros.
+ ///
+ HammerBros,
+
+ ///
+ /// Goomba
+ ///
+ Goomba,
+
+ ///
+ /// Blooper/Bloober/Squid
+ ///
+ Blooper,
+
+ ///
+ /// Bullet Bill
+ ///
+ BulletBill,
+
+ ///
+ /// Yellow Koopa Paratroopa (flies in place).
+ ///
+ YellowKoopaParatroopa,
+
+ ///
+ /// Green Cheep-cheep (slow)
+ ///
+ GreenCheepCheep,
+
+ ///
+ /// Red Cheep-cheep (fast)
+ ///
+ RedCheepCheep,
+
+ ///
+ /// Podoboo (jumps up to height specified).
+ ///
+ Podoboo,
+
+ ///
+ /// Piranha Plant
+ ///
+ PiranhaPlant,
+
+ ///
+ /// Green Koopa Paratroopa (Leaping)
+ ///
+ GreenKoopaParatroopa,
+
+ ///
+ /// Red Koopa Paratroopa (flies vertically)
+ ///
+ RedKoopaParatroopa,
+
+ ///
+ /// Green Koopa Paratroopa (flies horizontally)
+ ///
+ GreenKoopaParatroopa2,
+
+ ///
+ /// Lakitu
+ ///
+ Lakitu,
+
+ ///
+ /// Spiny (Not intended for direct use).
+ ///
+ Spiny,
+
+ ///
+ /// Red Flying Cheep-cheeps (generator)
+ ///
+ RedFlyingCheepCheep = 0x14,
+
+ ///
+ /// Bowser's fire (generator)
+ ///
+ BowsersFire,
+
+ ///
+ /// Fireworks (generator)
+ ///
+ Fireworks,
+
+ ///
+ /// Bullet Bill/Cheep-cheep (generator)
+ ///
+ BulletBillOrCheepCheeps,
+
+ ///
+ /// Fire bar (clockwise)
+ ///
+ FireBarClockwise = 0x1B,
+
+ ///
+ /// Fast fire bar (clockwise)
+ ///
+ FastFireBarClockwise,
+
+ ///
+ /// Fire bar (counter-clockwise)
+ ///
+ FireBarCounterClockwise,
+
+ ///
+ /// Fast fire bar (counter-clockwise)
+ ///
+ FastFireBarCounterClockwise,
+
+ ///
+ /// Long fire bar (clockwise)
+ ///
+ LongFireBarClockwise,
+
+ ///
+ /// Lift for balance ropes
+ ///
+ BalanceRopeLift = 0x24,
+
+ ///
+ /// Lift (moves down then back up)
+ ///
+ LiftDownThenUp,
+
+ ///
+ /// Lift (moves up)
+ ///
+ LiftUp,
+
+ ///
+ /// Lift (moves down)
+ ///
+ LiftDown,
+
+ ///
+ /// Lift (moves left then right)
+ ///
+ LiftLeftThenRight,
+
+ ///
+ /// Lift (falls)
+ ///
+ LiftFalling,
+
+ ///
+ /// Lift (moves right)
+ ///
+ LiftRight,
+
+ ///
+ /// Short lift (moves up)
+ ///
+ ShortLiftUp,
+
+ ///
+ /// Short lift (moved down)
+ ///
+ ShortLiftDown,
+
+ ///
+ /// Bowser
+ ///
+ Bowser,
+
+ ///
+ /// Warp zone command
+ ///
+ WarpZoneCommand = 0x34,
+
+ ///
+ /// Toad or princess (depends on world)
+ ///
+ ToadOrPrincess,
+
+ ///
+ /// 2 Goombas separated horizontally by 8 pixels (Y = 10)
+ ///
+ TwoGoombasY10 = 0x37,
+
+ ///
+ /// 2 Goombas separated horizontally by 8 pixels (Y = 10)
+ ///
+ ThreeGoombasY10,
+
+ ///
+ /// 3 Goombas separated horizontally by 8 pixels (Y = 10)
+ ///
+ TwoGoombasY6,
+
+ ///
+ /// 2 Goombas separated horizontally by 8 pixels (Y = 6)
+ ///
+ ThreeGoombasY6,
+
+ ///
+ /// 2 Green Koopa Troopas separated horizontally by 8 pixels (Y = 6)
+ ///
+ TwoGreenKoopasY10,
+
+ ///
+ /// 3 Green Koopa Troopas separated horizontally by 8 pixels (Y = 10)
+ ///
+ ThreeGreenKoopasY10,
+
+ ///
+ /// 2 Green Koopa Troopas separated horizontally by 8 pixels (Y = 6)
+ ///
+ TwoGreenKoopasY6,
+
+ ///
+ /// 3 Green Koopa Troopas separated horizontally by 8 pixels (Y = 6)
+ ///
+ ThreeGreenKoopasY6,
+
+ ScreenJump = 0x40,
+}
diff --git a/src/Smas/Smb1/AreaData/SpriteData/AreaSpriteCommand.cs b/src/Smas/Smb1/AreaData/SpriteData/AreaSpriteCommand.cs
new file mode 100644
index 0000000..00d3c2f
--- /dev/null
+++ b/src/Smas/Smb1/AreaData/SpriteData/AreaSpriteCommand.cs
@@ -0,0 +1,380 @@
+//
+// Copyright (c) 2022 spel werdz rite. All rights reserved. Licensed under GNU Affero
+// General Public License. See LICENSE in project root for full license
+// information, or visit https://www.gnu.org/licenses/#AGPL
+//
+
+namespace Maseya.Smas.Smb1.AreaData.SpriteData;
+
+using System;
+
+public struct AreaSpriteCommand : IEquatable
+{
+ public const byte TerminationCode = 0xFF;
+
+ public AreaSpriteCommand(byte value1, byte value2, byte value3 = 0)
+ {
+ Value1 = value1;
+ Value2 = value2;
+ Value3 = value3;
+ }
+
+ ///
+ /// Gets or sets the command value of this .
+ ///
+ public AreaSpriteCode Code
+ {
+ get
+ {
+ return IsThreeByteCommand
+ ? AreaSpriteCode.AreaPointer
+ : Y == 0x0F
+ ? AreaSpriteCode.ScreenJump
+ : (AreaSpriteCode)(Value2 & 0x3F);
+ }
+
+ set
+ {
+ Value2 &= 0xC0;
+ Value2 |= (byte)((int)value & 0x3F);
+ }
+ }
+
+ public bool IsValid
+ {
+ get
+ {
+ return Value1 != 0xFF && (IsThreeByteCommand || Value3 == 0);
+ }
+ }
+
+ ///
+ /// Gets or sets a value indicating whether this
+ /// only spawns after the hard world flag has been set.
+ ///
+ public bool HardWorldFlag
+ {
+ get
+ {
+ return !IsThreeByteCommand && (Value2 & 0x40) != 0;
+ }
+
+ set
+ {
+ if (IsThreeByteCommand)
+ {
+ return;
+ }
+
+ if (value)
+ {
+ Value2 |= 0x40;
+ }
+ else
+ {
+ Value2 &= 0xBF;
+ }
+ }
+ }
+
+ ///
+ /// Gets or sets a value indicating whether this
+ /// starts on the next page.
+ ///
+ public bool ScreenFlag
+ {
+ get
+ {
+ return (Value2 & 0x80) != 0;
+ }
+
+ set
+ {
+ if (value)
+ {
+ Value2 |= 0x80;
+ }
+ else
+ {
+ Value2 &= 0x7F;
+ }
+ }
+ }
+
+ ///
+ /// Gets the size, in bytes, of this .
+ ///
+ public int Size
+ {
+ get
+ {
+ return IsThreeByteCommand ? 3 : 2;
+ }
+ }
+
+ ///
+ /// Gets or sets the first value of this .
+ ///
+ public byte Value1
+ {
+ get;
+ set;
+ }
+
+ ///
+ /// Gets or sets the second value of this .
+ ///
+ public byte Value2
+ {
+ get;
+ set;
+ }
+
+ ///
+ /// Gets or sets the third value of this .
+ ///
+ public byte Value3
+ {
+ get;
+ set;
+ }
+
+ public int BaseCommand
+ {
+ get
+ {
+ return Value2 & 0x7F;
+ }
+ }
+
+ ///
+ /// Gets or sets the X-coordinate of this . The
+ /// coordinate is relative to the page the object is in.
+ ///
+ public int X
+ {
+ get
+ {
+ return Value1 >> 4;
+ }
+
+ set
+ {
+ Value1 &= 0x0F;
+ Value1 |= (byte)((value & 0x0F) << 4);
+ }
+ }
+
+ ///
+ /// Gets or sets the Y-coordinate of this .
+ ///
+ public int Y
+ {
+ get
+ {
+ return Value1 & 0x0F;
+ }
+
+ set
+ {
+ Value1 &= 0xF0;
+ Value1 |= (byte)(value & 0x0F);
+ }
+ }
+
+ public int RealY
+ {
+ get
+ {
+ return Y + (IsFirebar ? 0 : 1);
+ }
+
+ set
+ {
+ Y = value - (IsFirebar ? 0 : 1);
+ }
+ }
+
+ public int AreaNumber
+ {
+ get
+ {
+ return IsThreeByteCommand
+ ? Value2 & 0x7F
+ : 0;
+ }
+
+ set
+ {
+ if (IsThreeByteCommand)
+ {
+ Value3 &= 0x80;
+ Value3 |= (byte)(value & 0x7F);
+ }
+ }
+ }
+
+ public int WorldLimit
+ {
+ get
+ {
+ return IsThreeByteCommand
+ ? Value3 >> 5
+ : 0;
+ }
+
+ set
+ {
+ if (IsThreeByteCommand)
+ {
+ Value3 &= 0x0F;
+ Value3 |= (byte)((value & 7) << 4);
+ }
+ }
+ }
+
+ public int AreaPointerScreen
+ {
+ get
+ {
+ return IsThreeByteCommand
+ ? Value3 & 0x1F
+ : 0;
+ }
+
+ set
+ {
+ if (IsThreeByteCommand)
+ {
+ Value3 &= 0xE0;
+ Value3 |= (byte)(value & 0x1F);
+ }
+ }
+ }
+
+ public bool IsThreeByteCommand
+ {
+ get
+ {
+ return IsThreeByteSpecifier(Value1);
+ }
+ }
+
+ public string HexString
+ {
+ get
+ {
+ return $"{Value1:X2} {Value2:X2}"
+ + (IsThreeByteCommand ? $" {Value3:X2}" : String.Empty);
+ }
+ }
+
+ public string FullName
+ {
+ get
+ {
+ return Code switch
+ {
+ AreaSpriteCode.AreaPointer => $"Transition: Area Number={AreaNumber:X2}, Page={AreaPointerScreen + 1} (For world {1 + WorldLimit})",
+ AreaSpriteCode.GreenKoopaTroopa => "Koopa Troopa (Green)",
+ AreaSpriteCode.RedKoopaTroopa => "Koopa Troopa (Red; Walks off floors)",
+ AreaSpriteCode.BuzzyBeetle => "Buzzy Beetle",
+ AreaSpriteCode.RedKoopaTroopa2 => "Koopa Troopa (Red; Stays on floors)",
+ AreaSpriteCode.GreenKoopaTroopa2 => "Koopa Troopa (Green; Walks in place)",
+ AreaSpriteCode.HammerBros => "Hammer Bros.",
+ AreaSpriteCode.Goomba => "Goomba",
+ AreaSpriteCode.Blooper => "Squid",
+ AreaSpriteCode.BulletBill => "Bullet Bill",
+ AreaSpriteCode.YellowKoopaParatroopa => "Yellow Koopa Paratroopa (Flies in place)",
+ AreaSpriteCode.GreenCheepCheep => "Green Cheep-Cheep",
+ AreaSpriteCode.RedCheepCheep => "Red Cheep-Cheep",
+ AreaSpriteCode.Podoboo => "Podoboo",
+ AreaSpriteCode.PiranhaPlant => "Piranha Plant",
+ AreaSpriteCode.GreenKoopaParatroopa => "Green Koopa Paratroopa (Leaping)",
+ AreaSpriteCode.RedKoopaParatroopa => "Red Koopa Paratroopa (Flies vertically)",
+ AreaSpriteCode.GreenKoopaParatroopa2 => "Green Koopa Paratroopa (Flies horizontally)",
+ AreaSpriteCode.Lakitu => "Lakitu",
+ AreaSpriteCode.Spiny => "Spiny (undefined walk speed)",
+ AreaSpriteCode.RedFlyingCheepCheep => "Red Flying Cheep-Cheep",
+ AreaSpriteCode.BowsersFire => "Bowser's Fire (generator)",
+ AreaSpriteCode.Fireworks => "Single Firework",
+ AreaSpriteCode.BulletBillOrCheepCheeps => "Generator (Bullet Bill or Cheep-Cheeps)",
+ AreaSpriteCode.FireBarClockwise => "Fire Bar (Clockwise)",
+ AreaSpriteCode.FastFireBarClockwise => "Fire Bar (Fast; Clockwise)",
+ AreaSpriteCode.FireBarCounterClockwise => "Fire Bar (Counter-Clockwise)",
+ AreaSpriteCode.FastFireBarCounterClockwise => "Fire Bar (Fast; Counter-Clockwise)",
+ AreaSpriteCode.LongFireBarClockwise => "Long Fire Bar (Fast; Clockwise)",
+ AreaSpriteCode.BalanceRopeLift => "Rope for Lift Balance",
+ AreaSpriteCode.LiftDownThenUp => "Lift (Down, then up)",
+ AreaSpriteCode.LiftUp => "Lift (Up)",
+ AreaSpriteCode.LiftDown => "Lift (Down)",
+ AreaSpriteCode.LiftLeftThenRight => "Lift (Left, then right)",
+ AreaSpriteCode.LiftFalling => "Lift (Falling)",
+ AreaSpriteCode.LiftRight => "Lift (Right)",
+ AreaSpriteCode.ShortLiftUp => "Short Lift (Up)",
+ AreaSpriteCode.ShortLiftDown => "Short Lift (Down)",
+ AreaSpriteCode.Bowser => "Bowser: King of the Koopa",
+ AreaSpriteCode.WarpZoneCommand => "Command: Load Warp Zone",
+ AreaSpriteCode.ToadOrPrincess => "Toad or Princess",
+ AreaSpriteCode.TwoGoombasY10 => "Two Goombas (Y=10)",
+ AreaSpriteCode.ThreeGoombasY10 => "Three Goombas (Y=10)",
+ AreaSpriteCode.TwoGoombasY6 => "Two Goombas (Y=6)",
+ AreaSpriteCode.ThreeGoombasY6 => "Three Goombas (Y=6)",
+ AreaSpriteCode.TwoGreenKoopasY10 => "Two Green Koopa Troopas (Y=10)",
+ AreaSpriteCode.ThreeGreenKoopasY10 => "Three Green Koopa Troopas (Y=10)",
+ AreaSpriteCode.TwoGreenKoopasY6 => "Two Green Koopa Troopas (Y=6)",
+ AreaSpriteCode.ThreeGreenKoopasY6 => "Three Green Koopa Troopas (Y=6)",
+ AreaSpriteCode.ScreenJump => $"Page Skip: {BaseCommand & 0x1F}",
+ _ => $"Unknown command: {this}",
+ };
+ }
+ }
+
+ private bool IsFirebar
+ {
+ get
+ {
+ return ((uint)Code - (uint)AreaSpriteCode.FireBarClockwise) < 5;
+ }
+ }
+
+ public static bool operator ==(AreaSpriteCommand left, AreaSpriteCommand right)
+ {
+ return left.Equals(right);
+ }
+
+ public static bool operator !=(AreaSpriteCommand left, AreaSpriteCommand right)
+ {
+ return !(left == right);
+ }
+
+ public static bool IsThreeByteSpecifier(int coordinates)
+ {
+ return (coordinates & 0x0F) == 0x0E;
+ }
+
+ public override bool Equals(object? obj)
+ {
+ return obj is AreaSpriteCommand other && Equals(other);
+ }
+
+ public bool Equals(AreaSpriteCommand other)
+ {
+ return Value1.Equals(other.Value1) && Value2.Equals(other.Value2)
+ && (!IsThreeByteCommand || Value3.Equals(other.Value3));
+ }
+
+ public override int GetHashCode()
+ {
+ return IsThreeByteCommand
+ ? HashCode.Combine(Value1, Value2, Value3)
+ : HashCode.Combine(Value1, Value2);
+ }
+
+ public override string ToString()
+ {
+ return IsThreeByteCommand
+ ? $"({X}, {Y}): Area pointer to 0x{AreaNumber:X2}"
+ : $"({X}, {Y}): {Code}";
+ }
+}
diff --git a/src/Smas/Smb1/AreaData/SpriteData/AreaSpriteRenderer.cs b/src/Smas/Smb1/AreaData/SpriteData/AreaSpriteRenderer.cs
new file mode 100644
index 0000000..3345b1f
--- /dev/null
+++ b/src/Smas/Smb1/AreaData/SpriteData/AreaSpriteRenderer.cs
@@ -0,0 +1,1325 @@
+//
+// Copyright (c) 2022 spel werdz rite. All rights reserved. Licensed under GNU Affero
+// General Public License. See LICENSE in project root for full license
+// information, or visit https://www.gnu.org/licenses/#AGPL
+//
+
+namespace Maseya.Smas.Smb1.AreaData.SpriteData;
+
+using System.Collections.Generic;
+using System.Linq;
+
+using ObjectData;
+
+using Snes;
+
+using static System.Math;
+
+public class AreaSpriteRenderer
+{
+ private const int PixelStartIndex = GfxData.SpritePixelDataStartIndex / 0x40;
+ private const int PlayerPixelStartIndex = GfxData.MarioPixelDataStartIndex / 0x40;
+
+ public AreaSpriteRenderer()
+ {
+ Commands = new Dictionary()
+ {
+ { AreaSpriteCode.AreaPointer, AreaPointer },
+ { AreaSpriteCode.GreenKoopaTroopa, GreenKoopaTroopa },
+ { AreaSpriteCode.RedKoopaTroopa, RedKoopaTroopa },
+ { AreaSpriteCode.BuzzyBeetle, BuzzyBeetle },
+ { AreaSpriteCode.RedKoopaTroopa2, RedKoopaTroopa },
+ { AreaSpriteCode.GreenKoopaTroopa2, GreenKoopaTroopa },
+ { AreaSpriteCode.HammerBros, HammerBros },
+ { AreaSpriteCode.Goomba, Goomba },
+ { AreaSpriteCode.Blooper, Blooper },
+ { AreaSpriteCode.BulletBill, BulletBill },
+ { AreaSpriteCode.YellowKoopaParatroopa, GoldKoopaParaTroopa },
+ { AreaSpriteCode.GreenCheepCheep, GreenCheepCheep },
+ { AreaSpriteCode.RedCheepCheep, RedCheepCheep },
+ { AreaSpriteCode.Podoboo, Podoboo },
+ { AreaSpriteCode.PiranhaPlant, PiranhaPlant },
+ { AreaSpriteCode.GreenKoopaParatroopa, GreenKoopaParaTroopa },
+ { AreaSpriteCode.RedKoopaParatroopa, RedKoopaParaTroopa },
+ { AreaSpriteCode.GreenKoopaParatroopa2, GreenKoopaParaTroopa2 },
+ { AreaSpriteCode.Lakitu, Lakitu },
+ { AreaSpriteCode.Spiny, Spiny },
+ { AreaSpriteCode.RedFlyingCheepCheep, RedFlyingCheepCheep },
+ { AreaSpriteCode.BowsersFire, BowserFire },
+ { AreaSpriteCode.Fireworks, Firework },
+ { AreaSpriteCode.BulletBillOrCheepCheeps, BulletBill },
+ { AreaSpriteCode.FireBarClockwise, FireBarClockwise },
+ { AreaSpriteCode.FastFireBarClockwise, FireBarClockwiseFast },
+ { AreaSpriteCode.FireBarCounterClockwise, FireBarCounterClockwise },
+ { AreaSpriteCode.FastFireBarCounterClockwise, FireBarCounterClockwiseFast },
+ { AreaSpriteCode.LongFireBarClockwise, LongFireBarClockwiseFast},
+ { AreaSpriteCode.BalanceRopeLift, BalanceRopeLift },
+ { AreaSpriteCode.LiftDownThenUp, StationaryLift },
+ { AreaSpriteCode.LiftUp, LiftUp },
+ { AreaSpriteCode.LiftDown, LiftDown },
+ { AreaSpriteCode.LiftLeftThenRight, StationaryLift },
+ { AreaSpriteCode.LiftFalling, StationaryLift },
+ { AreaSpriteCode.LiftRight, StationaryLift },
+ { AreaSpriteCode.ShortLiftUp, ShortLiftUp },
+ { AreaSpriteCode.ShortLiftDown, ShortLiftDown },
+ { AreaSpriteCode.Bowser, Bowser },
+ { AreaSpriteCode.WarpZoneCommand, WarpZone },
+ { AreaSpriteCode.ToadOrPrincess, Toad },
+ { AreaSpriteCode.TwoGoombasY10, TwoGoombasY10 },
+ { AreaSpriteCode.ThreeGoombasY10, ThreeGoombasY10 },
+ { AreaSpriteCode.TwoGoombasY6, TwoGoombasY6 },
+ { AreaSpriteCode.ThreeGoombasY6, ThreeGoombasY6 },
+ { AreaSpriteCode.TwoGreenKoopasY10, TwoGreenKoopasY10 },
+ { AreaSpriteCode.ThreeGreenKoopasY10, ThreeGreenKoopasY10 },
+ { AreaSpriteCode.TwoGreenKoopasY6, TwoGreenKoopasY6 },
+ { AreaSpriteCode.ThreeGreenKoopasY6, ThreeGreenKoopasY6 },
+ };
+
+ ObjectCommands = new Dictionary()
+ {
+ { AreaObjectCode.EnterablePipe, PipePiranhaPlant },
+ { AreaObjectCode.UnenterablePipe, PipePiranhaPlant },
+ { AreaObjectCode.SpringBoard, SpringBoard },
+ { AreaObjectCode.FlagPole, FlagPole },
+ { AreaObjectCode.AltFlagPole, FlagPole },
+ { AreaObjectCode.BulletBillGenerator, BulletBill },
+ { AreaObjectCode.RedCheepCheepFlying, RedFlyingCheepCheep },
+ { AreaObjectCode.StopGenerator, StopGenerator },
+ { AreaObjectCode.LoopCommand, Loop },
+ { AreaObjectCode.ScrollStop, ScrollStop },
+ { AreaObjectCode.ScrollStopWarpZone, ScrollStopWarpZone },
+ { AreaObjectCode.AltScrollStop, ScrollStop },
+ };
+ }
+
+ private delegate IEnumerable SpriteCallback(int x, int y, int frame);
+
+ private Dictionary Commands
+ {
+ get;
+ }
+
+ private Dictionary ObjectCommands
+ {
+ get;
+ }
+
+ public static IEnumerable GetPlayerSprite(
+ int x,
+ int y,
+ Player player,
+ PlayerState state,
+ int frame)
+ {
+ var tile = new SpriteTile(PlayerPixelStartIndex, 0x0F, 9);
+
+ tile.TileIndex += frame * 8;
+
+ if (player == Player.Luigi)
+ {
+ tile.TileIndex += 0x100;
+ }
+
+ if (state == PlayerState.Small)
+ {
+ tile.TileIndex += 0x60;
+ }
+
+ for (var i = 0; i < 8; i++)
+ {
+ yield return new Sprite(
+ x + ((i & 1) << 3),
+ y + ((i & 6) << 2),
+ tile);
+ tile.TileIndex++;
+ }
+ }
+
+ public static IEnumerable GetObjectDataSprites(int[] tilemap)
+ {
+ var result = Enumerable.Empty();
+ for (var y = 0; y < 0x0C; y++)
+ {
+ var index = y * 0x200;
+ var pY = y << 4;
+ for (var x = 0; x < 0x200; x++)
+ {
+ var pX = x << 4;
+ switch (tilemap[index + x])
+ {
+ case 0xE8:
+ result = result.Concat(Powerup(pX, pY));
+ break;
+
+ case 0x62:
+ result = result.Concat(HiddenQuestionBlock(pX, pY));
+ break;
+
+ case 0x63:
+ result = result.Concat(HiddenBlock1UP(pX, pY));
+ break;
+
+ case 0x58:
+ result = result.Concat(Powerup(pX, pY));
+ break;
+
+ case 0x59:
+ result = result.Concat(BeanStalk(pX, pY));
+ break;
+
+ case 0x5A:
+ result = result.Concat(Star(pX, pY));
+ break;
+
+ case 0x5B:
+ result = result.Concat(Brick10Coins(pX, pY));
+ break;
+
+ case 0x5C:
+ result = result.Concat(Brick1Up(pX, pY));
+ break;
+ }
+ }
+ }
+
+ return result;
+ }
+
+ public IEnumerable GetSprites(
+ IEnumerable areaSpriteCommands,
+ IEnumerable areaObjectData,
+ int frame,
+ AreaType areaType,
+ bool showPipePiranhaPlants)
+ {
+ return Enumerable.Concat(
+ GetSprites(areaSpriteCommands, frame),
+ GetSprites(areaObjectData, frame, areaType, showPipePiranhaPlants));
+ }
+
+ public IEnumerable GetSprites(
+ IEnumerable areaObjectData,
+ int frame,
+ AreaType areaType,
+ bool showPipePiranhaPlants)
+ {
+ ObjectCommands[AreaObjectCode.BulletBillGenerator] = areaType == AreaType.Water
+ ? RedCheepCheep
+ : BulletBill;
+
+ var result = Enumerable.Empty();
+ var screen = 0;
+ foreach (var command in areaObjectData)
+ {
+ if (command.ScreenFlag)
+ {
+ screen += 0x10;
+ }
+ else if (command.Code == AreaObjectCode.ScreenJump)
+ {
+ screen = command.BaseCommand << 4;
+ }
+
+ if (!showPipePiranhaPlants && (
+ command.Code == AreaObjectCode.EnterablePipe
+ || command.Code == AreaObjectCode.UnenterablePipe))
+ {
+ continue;
+ }
+
+ var x = (screen | command.X) << 4;
+ var y = (command.Y + 2) << 4;
+ if (ObjectCommands.TryGetValue(command.Code, out var getSprites))
+ {
+ result = result.Concat(getSprites(x, y, frame));
+ }
+ }
+
+ return result;
+ }
+
+ public IEnumerable