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 GetSprites( + IEnumerable areaSpriteCommands, + int frame) + { + var screen = 0; + var screenSkip = false; + var lastAreaPointerX = -0x80; + var areaPointerOffset = 0; + + foreach (var command in areaSpriteCommands) + { + if (command.ScreenFlag && !screenSkip) + { + screen += 0x10; + } + + var x = (screen | command.X) << 4; + var y = (command.Y + 1) << 4; + if (command.Code == AreaSpriteCode.AreaPointer) + { + if (x - lastAreaPointerX < 0x80) + { + areaPointerOffset += 8; + y += areaPointerOffset; + } + else + { + areaPointerOffset = 0; + } + + lastAreaPointerX = x; + } + + screenSkip = command.Code == AreaSpriteCode.ScreenJump; + if (screenSkip) + { + screen = command.BaseCommand << 4; + } + + if (Commands.TryGetValue(command.Code, out var getSprites)) + { + foreach (var sprite in getSprites(x, y, frame)) + { + var result = sprite; + if (command.HardWorldFlag) + { + result.TileProperties |= TileProperties.Invert; + } + + yield return result; + } + } + } + } + + private static IEnumerable KoopaTroopa(int x, int y, int palette, int frame) + { + var index = (((frame + (x >> 1)) & 8) >> 3) * 5; + var tile = new ChrTile( + 0xA0 + index, + palette, + LayerPriority.Priority2, + TileFlip.Horizontal); + + y -= 7; + yield return new Sprite(x, y, new SpriteTile(tile, PixelStartIndex)); + + y += 8; + tile.TileIndex++; + yield return new Sprite(x + 8, y, new SpriteTile(tile, PixelStartIndex)); + + tile.TileIndex++; + yield return new Sprite(x, y, new SpriteTile(tile, PixelStartIndex)); + + y += 8; + tile.TileIndex++; + yield return new Sprite(x + 8, y, new SpriteTile(tile, PixelStartIndex)); + + tile.TileIndex++; + yield return new Sprite(x, y, new SpriteTile(tile, PixelStartIndex)); + } + + private static IEnumerable KoopaParaTroopa(int x, int y, int palette, int frame) + { + var index = ((frame + (x >> 1)) & 8) >> 3; + var tile = new ChrTile( + 0xA0 + (index * 5), + palette, + LayerPriority.Priority2, + TileFlip.Horizontal); + + y -= 7; + yield return new Sprite(x, y, new SpriteTile(tile, PixelStartIndex)); + + tile.TileIndex = 0x69 + (index << 1); + yield return new Sprite(x + 8, y, new SpriteTile(tile, PixelStartIndex)); + + y += 8; + tile.TileIndex = 0xA2 + (index * 5); + yield return new Sprite(x, y, new SpriteTile(tile, PixelStartIndex)); + + tile.TileIndex = 0x6A + (index * 2); + yield return new Sprite(x + 8, y, new SpriteTile(tile, PixelStartIndex)); + + y += 8; + tile.TileIndex = 0xA3 + (index * 5); + yield return new Sprite(x + 8, y, new SpriteTile(tile, PixelStartIndex)); + + tile.TileIndex++; + yield return new Sprite(x, y, new SpriteTile(tile, PixelStartIndex)); + } + + private static IEnumerable Text(int x, int y, int palette, string text) + { + var tile = new SpriteTile(0, palette, 11, false, false); + foreach (var c in text.ToUpper()) + { + if ((uint)(c - '0') <= 9) + { + tile.TileIndex = c - '0'; + } + else if ((uint)(c - 'A') <= 'Z' - 'A') + { + tile.TileIndex = c - 'A' + 10; + } + else if (c == '-') + { + tile.TileIndex = 0x28; + } + else if (c == '!') + { + tile.TileIndex = 0x2B; + } + else if (c == '#') + { + tile.TileIndex = 0x29; + } + else if (c == ' ') + { + tile.TileIndex = 0x24; + } + + yield return new Sprite(x, y, tile); + x += 8; + } + } + + private static IEnumerable CheepCheep(int x, int y, int palette, int frame) + { + var index = ((frame + (x << 1)) & 0x10) >> 4; + var tile = new ChrTile( + 0xB2 + (index * 4), + palette, + LayerPriority.Priority2, + TileFlip.Horizontal); + + yield return new Sprite(x + 8, y, new SpriteTile(tile, PixelStartIndex)); + + tile.TileIndex = 0xB3; + yield return new Sprite(x, y, new SpriteTile(tile, PixelStartIndex)); + + tile.TileIndex += 1 + (index * 3); + y += 8; + yield return new Sprite(x + 8, y, new SpriteTile(tile, PixelStartIndex)); + + tile.TileIndex = 0xB5; + yield return new Sprite(x, y, new SpriteTile(tile, PixelStartIndex)); + } + + private static IEnumerable FireBar( + int x, + int y, + int frame, + int rate, + int size) + { + var tile = new ChrTile( + 0xBE + ((frame & 4) >> 2), + 4, + LayerPriority.Priority2, + (TileFlip)((frame & 0x0C) >> 2)); + + x += 4; + y -= 12; + + // The exact rotation rate and movement is just arbitrarily made by me and does + // not yet follow what is in game. But this is where having math knowledge is + // nice, as I can make this look pretty good. The initial angle will depend on + // the x and y and location of the sprite, to give the visuals some variety. + var angle = (((frame & ~3) + x + y) >> 1) % rate * 2 * PI / rate; + for (var i = 0; i < size; i++) + { + yield return new Sprite( + x + (int)(8 * i * Cos(angle)), + y + (int)(8 * i * Sin(angle)), + new SpriteTile(tile, PixelStartIndex)); + } + } + + private static IEnumerable Lift(int x, int y, int size) + { + var tile = new ChrTile(0x87, 6, LayerPriority.Priority2, 0); + for (var i = 0; i <= 8 * size; i += 8) + { + yield return new Sprite(x + i, y, new SpriteTile(tile, PixelStartIndex)); + } + } + + private static IEnumerable Powerup(int x, int y) + { + return Mushroom(x, y, 6); + } + + private static IEnumerable Brick1Up(int x, int y) + { + return Mushroom(x, y, 5); + } + + private static IEnumerable HiddenBlock1UP(int x, int y) + { + return Mushroom(x, y, 5).Concat(HiddenQuestionBlock(x, y)); + } + + private static IEnumerable HiddenQuestionBlock(int x, int y) + { + // TODO(nrg): rip from tile data. + var tile = new Obj16Tile(0x0453, 0x0455, 0x0454, 0x0456); + yield return new Sprite( + x, y, new SpriteTile(tile[0], 0, 0), TileProperties.Transparent); + yield return new Sprite( + x, y + 8, new SpriteTile(tile[2], 0, 0), TileProperties.Transparent); + yield return new Sprite( + x + 8, y, new SpriteTile(tile[1], 0, 0), TileProperties.Transparent); + yield return new Sprite( + x + 8, y + 8, new SpriteTile(tile[3], 0, 0), TileProperties.Transparent); + } + + private static IEnumerable Mushroom(int x, int y, int palette) + { + var tile = new ChrTile(0xE9, palette, LayerPriority.Priority2, 0); + + y -= 8; + yield return new Sprite(x, y, new SpriteTile(tile, PixelStartIndex)); + + x += 8; + tile.TileIndex++; + yield return new Sprite(x, y, new SpriteTile(tile, PixelStartIndex)); + + y += 8; + tile.TileIndex = 0x79; + yield return new Sprite( + x, y, new SpriteTile(tile, PixelStartIndex), TileProperties.Transparent); + + x -= 8; + tile.TileIndex--; + yield return new Sprite( + x, y, new SpriteTile(tile, PixelStartIndex), TileProperties.Transparent); + } + + private static IEnumerable Coin(int x, int y) + { + var tile = new ChrTile(0x28, 2, LayerPriority.Priority2, 0); + + y -= 8; + yield return new Sprite( + x, y, new SpriteTile(tile, PixelStartIndex), TileProperties.Transparent); + + x += 8; + tile.TileIndex++; + yield return new Sprite( + x, y, new SpriteTile(tile, PixelStartIndex), TileProperties.Transparent); + + y += 8; + tile.TileIndex = 0x39; + yield return new Sprite( + x, y, new SpriteTile(tile, PixelStartIndex), TileProperties.Transparent); + + x -= 8; + tile.TileIndex--; + yield return new Sprite( + x, y, new SpriteTile(tile, PixelStartIndex), TileProperties.Transparent); + } + + private static IEnumerable HiddenCoinBlock(int x, int y) + { + return Coin(x, y).Concat(HiddenQuestionBlock(x, y)); + } + + private static IEnumerable Star(int x, int y) + { + var tile = new ChrTile(0x8D, 2, LayerPriority.Priority2, 0); + + y -= 8; + yield return new Sprite(x, y, new SpriteTile(tile, PixelStartIndex)); + + x += 8; + tile.XFlipped = true; + yield return new Sprite(x, y, new SpriteTile(tile, PixelStartIndex)); + + y += 8; + tile.TileIndex = 0xE4; + yield return new Sprite( + x, y, new SpriteTile(tile, PixelStartIndex), TileProperties.Transparent); + + x -= 8; + tile.XFlipped = false; + yield return new Sprite( + x, y, new SpriteTile(tile, PixelStartIndex), TileProperties.Transparent); + } + + private static int PiranhaPlantOffset(int cycle) + { + const int AnimationTime = 1 << 8; + const int AnimationTimeMask = AnimationTime - 1; + const int AscensionHeight = 0x18; + const int WaitTime = (AnimationTime / 2) - AscensionHeight; + const int MoveTime = AscensionHeight; + const int WaitOutsideFrame = WaitTime + MoveTime; + const int DescentEndFrame = WaitOutsideFrame + WaitTime; + // The Piranha Plant movement goes through a 256-frame animation cycle of + // coming out of the pipe, staying out, going back in, staying in, then + // repeating. The animation is arbitrarily made by me and does not yet follow + // whatever the animation cycle really is in-game. I just went for a reasonable + // approximation. + cycle &= AnimationTimeMask; + + return + // Time to stay in the pipe + cycle < WaitTime + ? 0 + + // Moving upward, out of the pipe. + : cycle < WaitOutsideFrame + ? cycle - WaitTime + + // Staying out of the pipe for a while. + : cycle < DescentEndFrame + ? AscensionHeight + + // Going back into the pipe. + : AscensionHeight - (cycle - DescentEndFrame); + } + + private static IEnumerable BeanStalk(int x, int y) + { + var tile = new ChrTile(0x12C, 5, LayerPriority.Priority3, 0); + + // Grow the bean stalk to the top of the screen. + for (y -= 0x10; y > 0x10; y -= 0x10) + { + tile.TileIndex = 0x12C; + yield return new Sprite(x, y, new SpriteTile(tile, PixelStartIndex)); + + tile.TileIndex++; + yield return new Sprite(x + 8, y, new SpriteTile(tile, PixelStartIndex)); + + tile.TileIndex += 0x0F; + yield return new Sprite(x, y + 8, new SpriteTile(tile, PixelStartIndex)); + + tile.TileIndex++; + yield return new Sprite( + x + 8, y + 8, new SpriteTile(tile, PixelStartIndex)); + } + + tile = new ChrTile(0x12A, 5, LayerPriority.Priority3, 0); + yield return new Sprite(x, y, new SpriteTile(tile, PixelStartIndex)); + + tile.TileIndex++; + yield return new Sprite(x + 8, y, new SpriteTile(tile, PixelStartIndex)); + + tile.TileIndex += 0x0F; + yield return new Sprite(x, y + 8, new SpriteTile(tile, PixelStartIndex)); + + tile.TileIndex++; + yield return new Sprite(x + 8, y + 8, new SpriteTile(tile, PixelStartIndex)); + } + + private static IEnumerable Brick10Coins(int x, int y) + { + x -= 4; + y -= 8; + var tile = new ChrTile(0x2C, 2, LayerPriority.Priority3, 0); + yield return new Sprite(x, y, new SpriteTile(tile, PixelStartIndex)); + + tile.TileIndex++; + yield return new Sprite(x + 8, y, new SpriteTile(tile, PixelStartIndex)); + + tile.TileIndex += 0x0F; + yield return new Sprite(x, y + 8, new SpriteTile(tile, PixelStartIndex)); + + tile.TileIndex++; + yield return new Sprite(x + 8, y + 8, new SpriteTile(tile, PixelStartIndex)); + + tile.TileIndex = 0x2E; + x += 8; + yield return new Sprite(x, y, new SpriteTile(tile, PixelStartIndex)); + + tile.TileIndex++; + yield return new Sprite(x + 8, y, new SpriteTile(tile, PixelStartIndex)); + + tile.TileIndex += 0x0F; + yield return new Sprite(x, y + 8, new SpriteTile(tile, PixelStartIndex)); + + tile.TileIndex++; + yield return new Sprite(x + 8, y + 8, new SpriteTile(tile, PixelStartIndex)); + } + + private IEnumerable GreenKoopaTroopa(int x, int y, int frame) + { + return KoopaTroopa(x, y, 5, frame); + } + + private IEnumerable RedKoopaTroopa(int x, int y, int frame) + { + return KoopaTroopa(x, y, 6, frame); + } + + private IEnumerable GreenKoopaParaTroopa(int x, int y, int frame) + { + return KoopaParaTroopa(x, y, 5, frame / 2); + } + + private IEnumerable GreenKoopaParaTroopa2(int x, int y, int frame) + { + return KoopaParaTroopa(x, y, 5, frame); + } + + private IEnumerable RedKoopaParaTroopa(int x, int y, int frame) + { + return KoopaParaTroopa(x, y, 6, frame); + } + + private IEnumerable GoldKoopaParaTroopa(int x, int y, int frame) + { + return KoopaParaTroopa(x, y, 7, frame); + } + + private IEnumerable Lakitu(int x, int y, int frame) + { + y -= 7; + var tile = new ChrTile(0xB8, 5, LayerPriority.Priority2, 0); + yield return new Sprite(x + 8, y, new SpriteTile(tile, PixelStartIndex)); + + tile.TileIndex++; + yield return new Sprite(x, y, new SpriteTile(tile, PixelStartIndex)); + + y += 8; + tile.TileIndex++; + yield return new Sprite(x + 8, y, new SpriteTile(tile, PixelStartIndex)); + + tile.TileIndex++; + yield return new Sprite(x, y, new SpriteTile(tile, PixelStartIndex)); + + y += 8; + tile.TileIndex++; + tile.XFlipped = true; + yield return new Sprite(x + 8, y, new SpriteTile(tile, PixelStartIndex)); + + tile.XFlipped = false; + yield return new Sprite(x, y, new SpriteTile(tile, PixelStartIndex)); + } + + private IEnumerable HammerBros(int x, int y, int frame) + { + y -= 7; + + // The hammer bros animation was pretty awful. Like other animations, this + // movement is all guess-work and does not yet emulate in-game animation. + + // Hammer Bros animation cycle. + const int cycle = 96; + + // Maximum total horizontal displacement from end to end. + const int distance = 8; + + // Moving the hammer bro left and right. + var offset = frame % cycle; + if (offset > cycle / 2) + { + // Move to the left until at max left displacement, then move to the right + // until back at center. + x += (int)(distance * ((offset - (3 * (cycle / 4.0))) / (cycle / 2.0))); + } + else + { + // Opposite of above movement. + x += (int)(distance * ((cycle / 4.0) - offset) / (cycle / 2.0)); + } + + var tile = new ChrTile(0x7C, 5, LayerPriority.Priority2, TileFlip.Horizontal); + yield return new Sprite(x, y, new SpriteTile(tile, PixelStartIndex)); + + tile.TileIndex++; + yield return new Sprite(x + 8, y, new SpriteTile(tile, PixelStartIndex)); + + // Adds arm for throwing hammer. + if (((frame + (x >> 1)) & 8) >> 3 == 0) + { + y += 8; + tile.TileIndex = 0x88; + yield return new Sprite(x, y, new SpriteTile(tile, PixelStartIndex)); + + tile.TileIndex++; + yield return new Sprite(x + 8, y, new SpriteTile(tile, PixelStartIndex)); + + y += 8; + tile.TileIndex++; + yield return new Sprite(x, y, new SpriteTile(tile, PixelStartIndex)); + } + else + { + y += 8; + tile.TileIndex = 0x8C; + yield return new Sprite(x, y, new SpriteTile(tile, PixelStartIndex)); + + tile.TileIndex = 0xD1; + yield return new Sprite(x + 8, y, new SpriteTile(tile, PixelStartIndex)); + + y += 8; + tile.TileIndex = 0xD2; + yield return new Sprite(x, y, new SpriteTile(tile, PixelStartIndex)); + } + + tile.TileIndex++; + yield return new Sprite(x + 8, y, new SpriteTile(tile, PixelStartIndex)); + } + + private IEnumerable Goomba(int x, int y, int frame) + { + y++; + var tile = new ChrTile(0xC6, 1, LayerPriority.Priority2, 0); + yield return new Sprite(x, y, new SpriteTile(tile, PixelStartIndex)); + + tile.TileIndex++; + yield return new Sprite(x + 8, y, new SpriteTile(tile, PixelStartIndex)); + + y += 8; + + // Walking animation. + if (((frame + (x >> 1)) & 8) >> 3 == 0) + { + tile.TileIndex++; + yield return new Sprite(x, y, new SpriteTile(tile, PixelStartIndex)); + + tile.TileIndex++; + yield return new Sprite(x + 8, y, new SpriteTile(tile, PixelStartIndex)); + } + else + { + tile.TileIndex += 2; + tile.XFlipped = true; + yield return new Sprite(x, y, new SpriteTile(tile, PixelStartIndex)); + + tile.TileIndex--; + yield return new Sprite(x + 8, y, new SpriteTile(tile, PixelStartIndex)); + } + } + + private IEnumerable BuzzyBeetle(int x, int y, int frame) + { + y += 2; + var index = ((frame + (x << 1)) & 8) >> 1; + var tile = new ChrTile(0xAA + index, 5, LayerPriority.Priority2, TileFlip.Horizontal); + + yield return new Sprite(x + 8, y, new SpriteTile(tile, PixelStartIndex)); + + tile.TileIndex++; + yield return new Sprite(x, y, new SpriteTile(tile, PixelStartIndex)); + + tile.TileIndex++; + y += 8; + yield return new Sprite(x + 8, y, new SpriteTile(tile, PixelStartIndex)); + + tile.TileIndex++; + yield return new Sprite(x, y, new SpriteTile(tile, PixelStartIndex)); + } + + private IEnumerable Spiny(int x, int y, int frame) + { + y++; + var index = ((frame + (x << 1)) & 16) >> 2; + var tile = new ChrTile(0x96 + index, 6, LayerPriority.Priority2, TileFlip.Horizontal); + + yield return new Sprite(x + 8, y, new SpriteTile(tile, PixelStartIndex)); + + tile.TileIndex++; + yield return new Sprite(x, y, new SpriteTile(tile, PixelStartIndex)); + + tile.TileIndex++; + y += 8; + yield return new Sprite(x + 8, y, new SpriteTile(tile, PixelStartIndex)); + + tile.TileIndex++; + yield return new Sprite(x, y, new SpriteTile(tile, PixelStartIndex)); + } + + private IEnumerable Firework(int x, int y, int frame) + { + y++; + frame = (frame + x + y) % 90; + if (frame > 20) + { + //yield break; + } + + // Maybe set up a click to explode? + var index = 1;// frame < 2 ? 2 : frame < 4 ? 1 : 0; + + var tile = new ChrTile(0xCA + index, 4, LayerPriority.Priority2, 0); + + yield return new Sprite(x, y, new SpriteTile(tile, PixelStartIndex)); + + tile.XFlipped = true; + yield return new Sprite(x + 8, y, new SpriteTile(tile, PixelStartIndex)); + + tile.YFlipped = true; + yield return new Sprite(x + 8, y + 8, new SpriteTile(tile, PixelStartIndex)); + + tile.XFlipped = false; + yield return new Sprite(x, y + 8, new SpriteTile(tile, PixelStartIndex)); + } + + private IEnumerable Toad(int x, int y, int frame) + { + y -= 7; + var tile = new ChrTile(0xCD, 6, LayerPriority.Priority2, 0); + yield return new Sprite(x, y, new SpriteTile(tile, PixelStartIndex)); + + tile.XFlipped = true; + yield return new Sprite(x + 8, y, new SpriteTile(tile, PixelStartIndex)); + + y += 8; + tile.TileIndex++; + yield return new Sprite(x + 8, y, new SpriteTile(tile, PixelStartIndex)); + + tile.XFlipped = false; + yield return new Sprite(x, y, new SpriteTile(tile, PixelStartIndex)); + + y += 8; + tile.TileIndex++; + yield return new Sprite(x, y, new SpriteTile(tile, PixelStartIndex)); + + tile.XFlipped = true; + yield return new Sprite(x + 8, y, new SpriteTile(tile, PixelStartIndex)); + } + + private IEnumerable Bowser(int x, int y, int frame) + { + y -= 0x17; + var tile = new ChrTile(0x1C5, 0, LayerPriority.Priority2, TileFlip.Horizontal); + yield return new Sprite(x + 8, y, new SpriteTile(tile, PixelStartIndex)); + + tile.TileIndex--; + yield return new Sprite(x + 16, y, new SpriteTile(tile, PixelStartIndex)); + + y += 8; + tile.TileIndex = 0x1EF; + yield return new Sprite(x, y, new SpriteTile(tile, PixelStartIndex)); + + tile.TileIndex--; + yield return new Sprite(x + 8, y, new SpriteTile(tile, PixelStartIndex)); + + y += 8; + tile.TileIndex += 0x10; + yield return new Sprite(x + 8, y, new SpriteTile(tile, PixelStartIndex)); + + tile.TileIndex++; + yield return new Sprite(x, y, new SpriteTile(tile, PixelStartIndex)); + + y -= 8; + tile.TileIndex = 0x1C1; + yield return new Sprite(x + 16, y, new SpriteTile(tile, PixelStartIndex)); + + tile.TileIndex--; + yield return new Sprite(x + 24, y, new SpriteTile(tile, PixelStartIndex)); + + y += 8; + tile.TileIndex += 0x10; + yield return new Sprite(x + 24, y, new SpriteTile(tile, PixelStartIndex)); + + tile.TileIndex++; + yield return new Sprite(x + 16, y, new SpriteTile(tile, PixelStartIndex)); + + tile.TileIndex += 0x12; + for (var i = 0; i < 2; i++) + { + y += 8; + for (var j = 0; j < 4; j++) + { + yield return new Sprite(x + (j << 3), y, new SpriteTile(tile, PixelStartIndex)); + tile.TileIndex--; + } + + tile.TileIndex += 0x10 + 4; + } + } + + private IEnumerable BowserFire(int x, int y, int frame) + { + y++; + var index = (frame + (x << 1)) / 12 % 3; + var offsets = new int[] { 0, 3, 0x20 }; + + var tile = new ChrTile(0x186 + offsets[index], 0, LayerPriority.Priority2, 0); + yield return new Sprite(x, y, new SpriteTile(tile, PixelStartIndex)); + + tile.TileIndex++; + yield return new Sprite(x + 8, y, new SpriteTile(tile, PixelStartIndex)); + + tile.TileIndex++; + yield return new Sprite(x + 16, y, new SpriteTile(tile, PixelStartIndex)); + + tile.TileIndex += 0x10; + y += 8; + yield return new Sprite(x + 16, y, new SpriteTile(tile, PixelStartIndex)); + + tile.TileIndex--; + yield return new Sprite(x + 8, y, new SpriteTile(tile, PixelStartIndex)); + + tile.TileIndex--; + yield return new Sprite(x, y, new SpriteTile(tile, PixelStartIndex)); + } + + private IEnumerable GreenCheepCheep(int x, int y, int frame) + { + return CheepCheep(x, y, 5, frame); + } + + private IEnumerable RedFlyingCheepCheep(int x, int y, int frame) + { + return CheepCheep(x, y, 6, (int)(1.5 * frame)); + } + + private IEnumerable StopGenerator(int x, int y, int frame) + { + return Text(x, y + 8, 2, "Stop Generator"); + } + + private IEnumerable Loop(int x, int y, int frame) + { + return Text(x, y + 8, 2, "Loop"); + } + + private IEnumerable ScrollStop(int x, int y, int frame) + { + return Text(x, y, 1, "Scroll Stop"); + } + + private IEnumerable AreaPointer(int x, int y, int frame) + { + return Text(x, y, 1, "Area Pointer"); + } + + private IEnumerable WarpZone(int x, int y, int frame) + { + return Text(x, y, 1, "Warpzone"); + } + + private IEnumerable ScrollStopWarpZone(int x, int y, int frame) + { + return Text(x, y, 1, "Scroll Stop Warpzone"); + } + + private IEnumerable RedCheepCheep(int x, int y, int frame) + { + return CheepCheep(x, y, 6, frame); + } + + private IEnumerable Podoboo(int x, int y, int frame) + { + var index = 0;// (((frame + x + y) >> 2) % 6) & ~1; + var tile = new ChrTile(0x162 + index, 2, LayerPriority.Priority2, 0); + + yield return new Sprite(x, y, new SpriteTile(tile, PixelStartIndex)); + + tile.TileIndex++; + yield return new Sprite(x + 8, y, new SpriteTile(tile, PixelStartIndex)); + + tile.TileIndex += 0x10; + yield return new Sprite(x + 8, y + 8, new SpriteTile(tile, PixelStartIndex)); + + tile.TileIndex--; + yield return new Sprite(x, y + 8, new SpriteTile(tile, PixelStartIndex)); + } + + private IEnumerable FireBarClockwise(int x, int y, int frame) + { + return FireBar(x, y, frame, 0x70, 6); + } + + private IEnumerable FireBarCounterClockwise(int x, int y, int frame) + { + return FireBar(x, y, frame, -0x70, 6); + } + + private IEnumerable FireBarClockwiseFast(int x, int y, int frame) + { + return FireBar(x, y, frame, 0x64, 6); + } + + private IEnumerable FireBarCounterClockwiseFast(int x, int y, int frame) + { + return FireBar(x, y, frame, -0x64, 6); + } + + private IEnumerable LongFireBarClockwiseFast(int x, int y, int frame) + { + return FireBar(x, y, frame, 0x68, 12); + } + + private IEnumerable TwoGoombasY10(int x, int y, int frame) + { + return Goombas(x, 2, 0xC0, frame); + } + + private IEnumerable ThreeGoombasY10(int x, int y, int frame) + { + return Goombas(x, 3, 0xC0, frame); + } + + private IEnumerable TwoGoombasY6(int x, int y, int frame) + { + return Goombas(x, 2, 0x80, frame); + } + + private IEnumerable ThreeGoombasY6(int x, int y, int frame) + { + return Goombas(x, 3, 0x80, frame); + } + + private IEnumerable TwoGreenKoopasY10(int x, int y, int frame) + { + return GreenKoopas(x, 2, 0xC0, frame); + } + + private IEnumerable ThreeGreenKoopasY10(int x, int y, int frame) + { + return GreenKoopas(x, 3, 0xC0, frame); + } + + private IEnumerable TwoGreenKoopasY6(int x, int y, int frame) + { + return GreenKoopas(x, 2, 0x80, frame); + } + + private IEnumerable ThreeGreenKoopasY6(int x, int y, int frame) + { + return GreenKoopas(x, 3, 0x80, frame); + } + + private IEnumerable Goombas(int x, int count, int y, int frame) + { + x -= 0x30; + for (var i = 0; i < count; i++, x += 0x18) + { + foreach (var sprite in Goomba(x, y, frame)) + { + yield return sprite; + } + } + } + + private IEnumerable GreenKoopas(int x, int count, int y, int frame) + { + x -= 0x30; + for (var i = 0; i < count; i++, x += 0x18) + { + foreach (var sprite in GreenKoopaTroopa(x, y, frame)) + { + yield return sprite; + } + } + } + + private IEnumerable StationaryLift(int x, int y, int frame) + { + return Lift(x, y, 6); + } + + private IEnumerable LiftDown(int x, int y, int frame) + { + y += ((frame << 6) / 75) + (x >> 4); + y &= 0xFF; + x += 8; + return Lift(x, y, 6); + } + + private IEnumerable LiftUp(int x, int y, int frame) + { + y -= ((frame << 6) / 75) + (x >> 4); + y &= 0xFF; + x += 8; + return Lift(x, y, 6); + } + + private IEnumerable ShortLiftDown(int x, int y, int frame) + { + y += ((frame << 6) / 75) + (x >> 4); + y &= 0xFF; + x += 8; + return Lift(x, y, 3); + } + + private IEnumerable ShortLiftUp(int x, int y, int frame) + { + y -= ((frame << 6) / 75) + (x >> 4); + y &= 0xFF; + x += 8; + return Lift(x, y, 3); + } + + private IEnumerable BalanceRopeLift(int x, int y, int frame) + { + x -= 4; + y -= 0x10; + return Lift(x, y, 6); + } + + private IEnumerable BulletBill(int x, int y, int frame) + { + var tile = new ChrTile(0x65, 5, LayerPriority.Priority2, TileFlip.Horizontal); + yield return new Sprite(x, y, new SpriteTile(tile, PixelStartIndex)); + + tile.TileIndex--; + yield return new Sprite(x + 8, y, new SpriteTile(tile, PixelStartIndex)); + + tile.TileIndex += 0x10; + yield return new Sprite(x + 8, y + 8, new SpriteTile(tile, PixelStartIndex)); + + tile.TileIndex++; + yield return new Sprite(x, y + 8, new SpriteTile(tile, PixelStartIndex)); + } + + private IEnumerable Blooper(int x, int y, int frame) + { + // I'm actually kinda proud of this little animation. + const int descentTime = 60; + const int ascentTime = 80; + const int totalTime = ascentTime + descentTime; + const int distance = 12; + var relativeFrame = (frame + x + y) % totalTime; + var isAscending = relativeFrame >= descentTime; + if (isAscending) + { + relativeFrame -= descentTime; + y += (int)(distance * ((ascentTime - relativeFrame) / ((float)ascentTime))); + } + else + { + y += (int)(distance * (relativeFrame / (float)descentTime)); + } + + y++; + var tile = new ChrTile(0xDC, 2, LayerPriority.Priority2, 0); + yield return new Sprite(x, y, new SpriteTile(tile, PixelStartIndex)); + + tile.XFlipped = true; + yield return new Sprite(x + 8, y, new SpriteTile(tile, PixelStartIndex)); + + y += 8; + if (isAscending) + { + tile.TileIndex++; + tile.XFlipped = false; + yield return new Sprite(x, y, new SpriteTile(tile, PixelStartIndex)); + + tile.XFlipped = true; + yield return new Sprite(x + 8, y, new SpriteTile(tile, PixelStartIndex)); + + y += 8; + tile.TileIndex++; + tile.XFlipped = false; + yield return new Sprite(x, y, new SpriteTile(tile, PixelStartIndex)); + + tile.XFlipped = true; + yield return new Sprite(x + 8, y, new SpriteTile(tile, PixelStartIndex)); + } + else + { + tile.TileIndex = 0xDF; + tile.XFlipped = false; + yield return new Sprite(x, y, new SpriteTile(tile, PixelStartIndex)); + + tile.XFlipped = true; + yield return new Sprite(x + 8, y, new SpriteTile(tile, PixelStartIndex)); + } + } + + private IEnumerable PipePiranhaPlant(int x, int y, int frame) + { + return PiranhaPlant(x + 8, y + 8, frame); + } + + private IEnumerable PiranhaPlant(int x, int y, int frame) + { + var index = ((frame + (x >> 1)) & 0x10) == 0 ? 0xE5 : 0xEC; + var offset = PiranhaPlantOffset(frame - (x >> 2)); + if (offset == 0) + { + yield break; + } + + y -= 8 + offset; + var tile = new ChrTile(index, 5, 0, 0); + yield return new Sprite(x, y, new SpriteTile(tile, PixelStartIndex)); + + tile.TileIndex++; + yield return new Sprite(x, y + 8, new SpriteTile(tile, PixelStartIndex)); + + tile.TileIndex--; + tile.TileFlip = TileFlip.Horizontal; + x += 8; + yield return new Sprite(x, y, new SpriteTile(tile, PixelStartIndex)); + + tile.TileIndex++; + yield return new Sprite(x, y + 8, new SpriteTile(tile, PixelStartIndex)); + + y += 0x10; + tile.TileIndex = 0xEB; + tile.PaletteIndex = 2; + yield return new Sprite(x, y, new SpriteTile(tile, PixelStartIndex)); + + tile.TileFlip = 0; + yield return new Sprite(x - 8, y, new SpriteTile(tile, PixelStartIndex)); + } + + private IEnumerable SpringBoard(int x, int y, int frame) + { + var tile = new ChrTile(0xF2, 6, LayerPriority.Priority2, 0); + yield return new Sprite(x, y, new SpriteTile(tile, PixelStartIndex)); + + tile.TileFlip = TileFlip.Horizontal; + yield return new Sprite(x + 8, y, new SpriteTile(tile, PixelStartIndex)); + + tile.TileFlip = TileFlip.Veritcal; + yield return new Sprite(x, y + 0x10, new SpriteTile(tile, PixelStartIndex)); + + tile.TileFlip = TileFlip.Both; + yield return new Sprite(x + 8, y + 0x10, new SpriteTile(tile, PixelStartIndex)); + + tile.TileIndex++; + tile.TileFlip = 0; + yield return new Sprite(x, y + 8, new SpriteTile(tile, PixelStartIndex)); + + tile.TileFlip = TileFlip.Horizontal; + yield return new Sprite(x + 8, y + 8, new SpriteTile(tile, PixelStartIndex)); + } + + private IEnumerable FlagPole(int x, int y, int frame) + { + y = 0x30; + x += 8; + + var index = 4 - (((frame + (x >> 1)) / 12 % 3) << 1); + + var tile = new ChrTile(0x120 + index, 5, LayerPriority.Priority2, 0); + yield return new Sprite(x, y, new SpriteTile(tile, PixelStartIndex)); + + tile.TileIndex++; + yield return new Sprite(x + 8, y, new SpriteTile(tile, PixelStartIndex)); + + tile.TileIndex += 0x0F; + yield return new Sprite(x, y + 8, new SpriteTile(tile, PixelStartIndex)); + + tile.TileIndex++; + yield return new Sprite(x + 8, y + 8, new SpriteTile(tile, PixelStartIndex)); + + // The flag at the castle is also drawn by the flagpole. For this reason, it is + // imperative that castle be a specific distance from the flagpole. + x += 0x60; + y += 0x40; + index = ((frame + (x >> 1)) >> 2) & 6; + tile = new ChrTile(0x104 + index, 5, LayerPriority.Priority0, 0); + yield return new Sprite(x, y, new SpriteTile(tile, PixelStartIndex)); + + tile.TileIndex++; + yield return new Sprite(x + 8, y, new SpriteTile(tile, PixelStartIndex)); + + tile.TileIndex += 0x0F; + yield return new Sprite(x, y + 8, new SpriteTile(tile, PixelStartIndex)); + + tile.TileIndex++; + yield return new Sprite(x + 8, y + 8, new SpriteTile(tile, PixelStartIndex)); + } +} diff --git a/src/Smas/Smb1/ExtensionMethods.cs b/src/Smas/Smb1/ExtensionMethods.cs new file mode 100644 index 0000000..ff6c704 --- /dev/null +++ b/src/Smas/Smb1/ExtensionMethods.cs @@ -0,0 +1,697 @@ +// +// 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; + +using System.Collections.Generic; +using System.Collections.Immutable; + +using AreaData.ObjectData; +using AreaData.SpriteData; + +public static class ExtensionMethods +{ + private static readonly ImmutableHashSet HorizontallyExtendableObjects = + ImmutableHashSet.Create( + AreaObjectCode.AreaSpecificPlatform, + AreaObjectCode.GreenIsland, + AreaObjectCode.MushroomIsland, + AreaObjectCode.CloudGround, + AreaObjectCode.HorizontalBricks, + AreaObjectCode.HorizontalStones, + AreaObjectCode.HorizontalCoins, + AreaObjectCode.Hole, + AreaObjectCode.BalanceHorizontalRope, + AreaObjectCode.BridgeV7, + AreaObjectCode.BridgeV8, + AreaObjectCode.BridgeV10, + AreaObjectCode.HoleWithWaterOrLava, + AreaObjectCode.HorizontalQuestionBlocksV3, + AreaObjectCode.HorizontalQuestionBlocksV7, + AreaObjectCode.Staircase); + + private static readonly ImmutableHashSet VerticallyExtendableObjects = + ImmutableHashSet.Create( + AreaObjectCode.VerticalBricks, + AreaObjectCode.VerticalStones, + AreaObjectCode.UnenterablePipe, + AreaObjectCode.EnterablePipe, + AreaObjectCode.RopeForLift, + AreaObjectCode.PulleyRope, + AreaObjectCode.Castle, + AreaObjectCode.CastleCeilingCap, + AreaObjectCode.Staircase, + AreaObjectCode.VerticalSeaBlocks, + AreaObjectCode.ExtendableJPipe, + AreaObjectCode.VerticalBalls); + + private static readonly ImmutableHashSet ExtendableObjects = + HorizontallyExtendableObjects + .Union(VerticallyExtendableObjects) + .Add(AreaObjectCode.ScreenJump); + + public static IEnumerable ToBytes( + this IEnumerable items) + { + foreach (var command in items) + { + yield return command.Value1; + yield return command.Value2; + if (command.IsThreeByteCommand) + { + yield return command.Value3; + } + } + + yield return AreaObjectCommand.TerminationCode; + } + + public static IEnumerable ToBytes( + this IEnumerable items) + { + foreach (var command in items) + { + yield return command.Value1; + yield return command.Value2; + if (command.IsThreeByteCommand) + { + yield return command.Value3; + } + } + + yield return AreaSpriteCommand.TerminationCode; + } + + public static AreaObjectCode ToObjectCode(this AreaPlatformType type) + { + return AreaObjectCode.AreaSpecificPlatform | (AreaObjectCode)(8 | (int)type); + } + + public static bool IsHorizontallyExtendableObject(this AreaObjectCode code) + { + return HorizontallyExtendableObjects.Contains(code); + } + + public static bool IsVerticallyExtendableObject(this AreaObjectCode code) + { + return VerticallyExtendableObjects.Contains(code); + } + + public static bool IsExtendableObject(this AreaObjectCode code) + { + return ExtendableObjects.Contains(code); + } + + public static string BaseName(this AreaObjectCode code) + { + switch (code) + { + case AreaObjectCode.QuestionBlockPowerup: + return AddDescriptor( + Resources.QuestionBlock, + Resources.Powerup); + + case AreaObjectCode.QuestionBlockCoin: + return AddDescriptor( + Resources.QuestionBlock, + Resources.Coin); + + case AreaObjectCode.HiddenBlockCoin: + return AddDescriptor( + Resources.HiddenBlock, + Resources.Coin); + + case AreaObjectCode.HiddenBlock1UP: + return AddDescriptor( + Resources.HiddenBlock, + Resources.LifeMushroom); + + case AreaObjectCode.BrickPowerup: + return AddDescriptor( + Resources.Brick, + Resources.Powerup); + + case AreaObjectCode.BrickBeanstalk: + return AddDescriptor( + Resources.Brick, + Resources.Beanstalk); + + case AreaObjectCode.BrickStar: + return AddDescriptor( + Resources.Brick, + Resources.Star); + + case AreaObjectCode.Brick10Coins: + return AddDescriptor( + Resources.Brick, + Resources.TenCoins); + + case AreaObjectCode.Brick1UP: + return AddDescriptor( + Resources.Brick, + Resources.LifeMushroom); + + case AreaObjectCode.SidewaysPipe: + return Resources.SidewaysPipe; + + case AreaObjectCode.UsedBlock: + return Resources.UsedBlock; + + case AreaObjectCode.SpringBoard: + return Resources.SpringBoard; + + case AreaObjectCode.JPipe: + case AreaObjectCode.AltJPipe: + return Resources.JPipe; + + case AreaObjectCode.FlagPole: + case AreaObjectCode.AltFlagPole: + return Resources.FlagPole; + + case AreaObjectCode.Empty: + case AreaObjectCode.Empty2: + return Resources.Empty; + + case AreaObjectCode.AreaSpecificPlatform: + return Resources.AreaSpecificPlatform; + + case AreaObjectCode.GreenIsland: + return Resources.AreaSpecificPlatform_Trees; + + case AreaObjectCode.MushroomIsland: + return Resources.AreaSpecificPlatform_Mushrooms; + + case AreaObjectCode.Cannon: + return Resources.AreaSpecificPlatform_BulletBillTurrets; + + case AreaObjectCode.CloudGround: + // This is not a mistake. The regular ground changes to clouds, and + // area specific platform is still trees. + return Resources.AreaSpecificPlatform_Trees; + + case AreaObjectCode.HorizontalBricks: + return Resources.HorizontalBricks; + + case AreaObjectCode.HorizontalStones: + return Resources.HorizontalStones; + + case AreaObjectCode.HorizontalCoins: + return Resources.HorizontalCoins; + + case AreaObjectCode.VerticalBricks: + return Resources.VerticalBricks; + + case AreaObjectCode.VerticalStones: + return Resources.VerticalStones; + + case AreaObjectCode.UnenterablePipe: + return Resources.UnenterablePipe; + + case AreaObjectCode.EnterablePipe: + return Resources.EnterablePipe; + + case AreaObjectCode.Hole: + return Resources.Hole; + + case AreaObjectCode.BalanceHorizontalRope: + return Resources.BalanceHorizontalRope; + + case AreaObjectCode.BridgeV7: + return AddYDescriptor(Resources.Bridge, 7); + + case AreaObjectCode.BridgeV8: + return AddYDescriptor(Resources.Bridge, 8); + + case AreaObjectCode.BridgeV10: + return AddYDescriptor(Resources.Bridge, 10); + + case AreaObjectCode.HoleWithWaterOrLava: + return Resources.HoleWithWaterOrLava; + + case AreaObjectCode.HorizontalQuestionBlocksV3: + return AddYDescriptor(Resources.HorizontalQuestionBlocks, 3); + + case AreaObjectCode.HorizontalQuestionBlocksV7: + return AddYDescriptor(Resources.HorizontalQuestionBlocks, 7); + + case AreaObjectCode.ScreenJump: + return Resources.ScreenJump; + + case AreaObjectCode.BowserAxe: + return Resources.BowserAxe; + + case AreaObjectCode.RopeForAxe: + return Resources.RopeForAxe; + + case AreaObjectCode.BowserBridge: + return Resources.BowserBridge; + + case AreaObjectCode.ScrollStopWarpZone: + return Resources.ScrollStopWarpZone; + + case AreaObjectCode.ScrollStop: + case AreaObjectCode.AltScrollStop: + return Resources.ScrollStop; + + case AreaObjectCode.RedCheepCheepFlying: + return Resources.RedCheepCheepFlying; + + case AreaObjectCode.BulletBillGenerator: + return Resources.BulletBillGenerator; + + case AreaObjectCode.StopGenerator: + return Resources.StopGenerator; + + case AreaObjectCode.LoopCommand: + return Resources.LoopCommand; + + case AreaObjectCode.TerrainAndBackgroundSceneryChange: + return Resources.BrickAndSceneryChange; + + case AreaObjectCode.ForegroundChange: + return Resources.ForegroundChange; + + case AreaObjectCode.RopeForLift: + return Resources.RopeForLift; + + case AreaObjectCode.PulleyRope: + return Resources.PulleyRope; + + case AreaObjectCode.EmptyTile: + return Resources.EmptyTile; + + case AreaObjectCode.Castle: + return Resources.Castle; + + case AreaObjectCode.CastleCeilingCap: + return Resources.CastleCeilingCap; + + case AreaObjectCode.Staircase: + return Resources.Staircase; + + case AreaObjectCode.CastleStairs: + return Resources.CastleStairs; + + case AreaObjectCode.CastleRectangularCeilingTiles: + return Resources.CastleRectangularCeilingTiles; + + case AreaObjectCode.CastleFloorRightEdge: + return Resources.CastleFloorRightEdge; + + case AreaObjectCode.CastleFloorLeftEdge: + return Resources.CastleFloorLeftEdge; + + case AreaObjectCode.CastleFloorLeftWall: + return Resources.CastleFloorLeftWall; + + case AreaObjectCode.CastleFloorRightWall: + return Resources.CastleFloorRightWall; + + case AreaObjectCode.VerticalSeaBlocks: + return Resources.VerticalSeaBlocks; + + case AreaObjectCode.ExtendableJPipe: + return Resources.ExtendableJPipe; + + case AreaObjectCode.VerticalBalls: + return Resources.VerticalBalls; + + default: + return String.Format( + Resources.UnknownCommand, + ((int)code).ToString("X4")); + } + } + + public static int GetMaxLength(this AreaObjectCode code) + { + switch (code) + { + case AreaObjectCode.ScreenJump: + return 0x1F; + + case AreaObjectCode.EnterablePipe: + case AreaObjectCode.UnenterablePipe: + return 7; + + case AreaObjectCode.Staircase: + return 8; + + case AreaObjectCode.Castle: + return 7; + + default: + return code.IsExtendableObject() ? 0x0F : 0; + } + } + + public static string BaseName(this AreaSpriteCode code) + { + switch (code) + { + case AreaSpriteCode.AreaPointer: + return Resources.AreaPointer; + + case AreaSpriteCode.GreenKoopaTroopa: + return AddDescriptor( + Resources.KoopaTroopa, + Resources.Green); + + case AreaSpriteCode.RedKoopaTroopa: + return AddDescriptor( + Resources.KoopaTroopa, + Resources.Red); + + case AreaSpriteCode.BuzzyBeetle: + return Resources.BuzzyBeetle; + + case AreaSpriteCode.RedKoopaTroopa2: + return AddTwoDescriptors( + Resources.KoopaParatroopa, + Resources.Red, + Resources.WalksOffFloors); + + case AreaSpriteCode.GreenKoopaTroopa2: + return AddTwoDescriptors( + Resources.KoopaParatroopa, + Resources.Green, + Resources.WalksInPlace); + + case AreaSpriteCode.HammerBros: + return Resources.HammerBros; + + case AreaSpriteCode.Goomba: + return Resources.Goomba; + + case AreaSpriteCode.Blooper: + return Resources.Blooper; + + case AreaSpriteCode.BulletBill: + return Resources.BulletBill; + + case AreaSpriteCode.YellowKoopaParatroopa: + return AddTwoDescriptors( + Resources.KoopaParatroopa, + Resources.Yellow, + WarningDescriptor(Resources.FliesInPlace)); + + case AreaSpriteCode.GreenCheepCheep: + return AddDescriptor( + Resources.CheepCheep, + Resources.Green); + + case AreaSpriteCode.RedCheepCheep: + return AddDescriptor( + Resources.CheepCheep, + Resources.Red); + + case AreaSpriteCode.Podoboo: + return Resources.Podoboo; + + case AreaSpriteCode.PiranhaPlant: + return Resources.PiranhaPlant; + + case AreaSpriteCode.GreenKoopaParatroopa: + return AddTwoDescriptors( + Resources.KoopaParatroopa, + Resources.Green, + Resources.Leaping); + + case AreaSpriteCode.RedKoopaParatroopa: + return AddTwoDescriptors( + Resources.KoopaParatroopa, + Resources.Red, + Resources.FliesVertically); + + case AreaSpriteCode.GreenKoopaParatroopa2: + return AddTwoDescriptors( + Resources.KoopaParatroopa, + Resources.Green, + Resources.FliesHorizontally); + + case AreaSpriteCode.Lakitu: + return Resources.Lakitu; + + case AreaSpriteCode.Spiny: + return AddWarningDescriptor( + Resources.Spiny, + Resources.RandomWalkSpeed); + + case AreaSpriteCode.RedFlyingCheepCheep: + return AddTwoDescriptors( + Resources.CheepCheep, + Resources.Red, + Resources.Flying); + + case AreaSpriteCode.BowsersFire: + return AddDescriptor( + Resources.BowserFire, + Resources.Generator); + + case AreaSpriteCode.Fireworks: + return Resources.Firework; + + case AreaSpriteCode.BulletBillOrCheepCheeps: + return AddDescriptor( + Resources.Generator, + Resources.BulletBillOrCheepCheep); + + case AreaSpriteCode.FireBarClockwise: + return AddDescriptor( + Resources.FireBar, + Resources.Clockwise); + + case AreaSpriteCode.FastFireBarClockwise: + return AddTwoDescriptors( + Resources.FireBar, + Resources.Fast, + Resources.Clockwise); + + case AreaSpriteCode.FireBarCounterClockwise: + return AddDescriptor( + Resources.FireBar, + Resources.CounterClockwise); + + case AreaSpriteCode.FastFireBarCounterClockwise: + return AddTwoDescriptors( + Resources.FireBar, + Resources.Fast, + Resources.CounterClockwise); + + case AreaSpriteCode.LongFireBarClockwise: + return AddTwoDescriptors( + Resources.FireBar, + Resources.Long, + Resources.CounterClockwise); + + case AreaSpriteCode.BalanceRopeLift: + return Resources.BalanceRopeLift; + + case AreaSpriteCode.LiftDownThenUp: + return AddTwoDescriptors( + Resources.Lift, + Resources.Down, + Resources.Up); + + case AreaSpriteCode.LiftUp: + return AddDescriptor( + Resources.Lift, + Resources.Up); + + case AreaSpriteCode.LiftDown: + return AddDescriptor( + Resources.Lift, + Resources.Down); + + case AreaSpriteCode.LiftLeftThenRight: + return AddTwoDescriptors( + Resources.Lift, + Resources.Left, + Resources.Right); + + case AreaSpriteCode.LiftFalling: + return AddDescriptor( + Resources.Lift, + Resources.Falling); + + case AreaSpriteCode.LiftRight: + return AddDescriptor( + Resources.Lift, + Resources.Right); + + case AreaSpriteCode.ShortLiftUp: + return AddTwoDescriptors( + Resources.Lift, + Resources.Short, + Resources.Up); + + case AreaSpriteCode.ShortLiftDown: + return AddTwoDescriptors( + Resources.Lift, + Resources.Short, + Resources.Down); + + case AreaSpriteCode.Bowser: + return Resources.Bowser; + + case AreaSpriteCode.WarpZoneCommand: + return Resources.WarpZoneCommand; + + case AreaSpriteCode.ToadOrPrincess: + return Resources.ToadOrPrincess; + + case AreaSpriteCode.TwoGoombasY10: + return AddYDescriptor(Resources.Goomba2, 10); + + case AreaSpriteCode.ThreeGoombasY10: + return AddYDescriptor(Resources.Goomba3, 10); + + case AreaSpriteCode.TwoGoombasY6: + return AddYDescriptor(Resources.Goomba2, 6); + + case AreaSpriteCode.ThreeGoombasY6: + return AddYDescriptor(Resources.Goomba3, 6); + + case AreaSpriteCode.TwoGreenKoopasY10: + return AddYDescriptor(Resources.GreenKoopaTroopa2, 10); + + case AreaSpriteCode.ThreeGreenKoopasY10: + return AddYDescriptor(Resources.GreenKoopaTroopa3, 10); + + case AreaSpriteCode.TwoGreenKoopasY6: + return AddYDescriptor(Resources.GreenKoopaTroopa2, 6); + + case AreaSpriteCode.ThreeGreenKoopasY6: + return AddYDescriptor(Resources.GreenKoopaTroopa3, 6); + + case AreaSpriteCode.ScreenJump: + return Resources.ScreenJump; + + default: + return String.Format( + Resources.UnknownCommand, + ((int)code).ToString("X2")); + } + } + + public static string GetDescription( + this AreaObjectCommand command, + AreaPlatformType areaPlatformType) + { + var length = command.Parameter + 1; + var code = command.Code; + switch (code) + { + case AreaObjectCode.AreaSpecificPlatform: + code = areaPlatformType.ToObjectCode(); + break; + + case AreaObjectCode.BridgeV7: + return AddTwoDescriptors( + Resources.Bridge, + YDescriptor(7), + WidthDescriptor(length)); + + case AreaObjectCode.BridgeV8: + return AddTwoDescriptors( + Resources.Bridge, + YDescriptor(8), + WidthDescriptor(length)); + + case AreaObjectCode.BridgeV10: + return AddTwoDescriptors( + Resources.Bridge, + YDescriptor(10), + WidthDescriptor(length)); + + case AreaObjectCode.HorizontalQuestionBlocksV3: + return AddTwoDescriptors( + Resources.HorizontalQuestionBlocks, + YDescriptor(3), + WidthDescriptor(length)); + + case AreaObjectCode.HorizontalQuestionBlocksV7: + return AddTwoDescriptors( + Resources.HorizontalQuestionBlocks, + YDescriptor(7), + WidthDescriptor(length)); + } + + var baseName = code.BaseName(); + return code.IsHorizontallyExtendableObject() + ? AddWidthDescriptor(baseName, length) + : code.IsVerticallyExtendableObject() + ? AddHeightDescriptor(baseName, length) + : code == AreaObjectCode.ScreenJump + ? AddDescriptor(baseName, PageSetDescriptor(length)) + : baseName; + } + + private static string AddDescriptor(string item, string description) + { + return String.Format( + Resources.ObjectWithDescriptor, + item, + description); + } + + private static string AddTwoDescriptors( + string item, + string description1, + string description2) + { + return String.Format( + Resources.ObjectWithTwoDescriptors, + item, + description1, + description2); + } + + private static string YDescriptor(object y) + { + return String.Format(Resources.WithSpecificYCoord, y); + } + + private static string HeightDescriptor(object height) + { + return String.Format(Resources.VerticallyExtendableObject, height); + } + + private static string WidthDescriptor(object width) + { + return String.Format(Resources.HorizontallyExtendableObject, width); + } + + private static string AddYDescriptor(string item, object y) + { + return AddDescriptor(item, YDescriptor(y)); + } + + private static string AddHeightDescriptor(string item, object height) + { + return AddDescriptor(item, HeightDescriptor(height)); + } + + private static string AddWidthDescriptor(string item, object width) + { + return AddDescriptor(item, WidthDescriptor(width)); + } + + private static string PageSetDescriptor(int page) + { + return String.Format(Resources.SetPage, page); + } + + private static string WarningDescriptor(string message) + { + return String.Format(Resources.WarningDescriptor, message); + } + + private static string AddWarningDescriptor(string item, string message) + { + return AddDescriptor(item, WarningDescriptor(message)); + } +} diff --git a/src/Smas/Smb1/GameData.cs b/src/Smas/Smb1/GameData.cs new file mode 100644 index 0000000..60ef4c2 --- /dev/null +++ b/src/Smas/Smb1/GameData.cs @@ -0,0 +1,53 @@ +namespace Maseya.Smas.Smb1; + +using AreaData; +using AreaData.ObjectData; +using AreaData.SpriteData; + +using Snes; + +public class GameData +{ + public GameData(Rom rom, Pointers? pointers = null) + { + pointers ??= Pointers.GetPointers(rom); + + PaletteData = new PaletteData(rom, pointers.PaletteDataPointers); + GfxData = new GfxData(rom, pointers.GfxDataPointers); + Map16Data = new Map16Data(rom, pointers.Map16DataPointers); + AreaLoader = new AreaLoader(rom, pointers.AreaLoaderPointers); + TilemapLoader = new TilemapLoader( + rom, + pointers.TilemapLoaderPointers, + AreaLoader.NumberOfAreas); + AreaObjectRenderer = new AreaObjectRenderer( + rom, + pointers.AreaObjectRendererPointers); + AreaSpriteRenderer = new AreaSpriteRenderer(); + } + + public PaletteData PaletteData { get; } + + public GfxData GfxData { get; } + + public Map16Data Map16Data { get; } + + public AreaLoader AreaLoader { get; } + + public TilemapLoader TilemapLoader { get; } + + public AreaObjectRenderer AreaObjectRenderer { get; } + + public AreaSpriteRenderer AreaSpriteRenderer { get; } + + public void WriteToGameData(Rom rom, Pointers? pointers = null) + { + pointers ??= Pointers.GetPointers(rom); + + AreaObjectRenderer.WriteToGameData(rom, pointers.AreaObjectRendererPointers); + AreaLoader.WriteToGameData(rom, pointers.AreaLoaderPointers); + Map16Data.WriteToGameData(rom, pointers.Map16DataPointers); + GfxData.WriteToGameData(rom, pointers.GfxDataPointers); + PaletteData.WriteToGameData(rom, pointers.PaletteDataPointers); + } +} diff --git a/src/Smas/Smb1/GfxData.cs b/src/Smas/Smb1/GfxData.cs new file mode 100644 index 0000000..c5c04d2 --- /dev/null +++ b/src/Smas/Smb1/GfxData.cs @@ -0,0 +1,356 @@ +// +// 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; + +using System; +using System.Collections.Generic; + +using Snes; + +using static Snes.GfxConverter; + +public class GfxData +{ + public const int SpritePixelDataStartIndex = + AreaBgPixelStartIndex + AreaBgPixelDataSize; + + public const int MarioPixelDataStartIndex = + AnimatedPixelDataStartIndex + SpritePixelDataSize; + + public const int TotalPixelDataSize = + MenuPixelDataStartIndex + MenuPixelDataSize; + + private const int AreaGfxSize = 0x4000; + private const int AnimatedPixelDataDestIndex = 0x3000; + private const int AreaPixelDataSize = AreaGfxSize << 1; + + private const int AreaBgGfxSize = 0x2000; + private const int AreaBgPixelDataSize = AreaBgGfxSize << 1; + private const int TilesetCount = 0x19; + + private const int SpriteGfxSize = 0x4000; + private const int SpritePixelDataSize = SpriteGfxSize << 1; + + private const int AnimatedGfxSize = 0x4000; + private const int AnimatedPixelDataSize = AnimatedGfxSize << 1; + private const int AnimatedPixelDataFrameSize = 0xC00; + private const int AnimatedPixelDataFrameOffset = 0x1000; + + private const int PlayerGfxSize = 0x2000; + private const int PlayerPixelDataSize = PlayerGfxSize << 1; + + private const int MenuGfxSize = 0x800; + private const int MenuPixelDataSize = MenuGfxSize << 2; + + private const int AreaPixelStartIndex = 0; + + private const int AreaBgPixelStartIndex = AreaPixelStartIndex + AreaPixelDataSize; + + private const int AnimatedPixelDataStartIndex = + SpritePixelDataStartIndex + SpritePixelDataSize; + + private const int LuigiPixelDataStartIndex = + MarioPixelDataStartIndex + PlayerPixelDataSize; + + private const int MenuPixelDataStartIndex = + LuigiPixelDataStartIndex + PlayerPixelDataSize; + + public GfxData(Rom rom, GfxDataPointers pointers) + { + AreaPixelData = GfxToPixelMap( + rom.ReadBytes(pointers.AreaGfxAddress, AreaGfxSize)); + SpritePixelData = GfxToPixelMap( + rom.ReadBytes(pointers.SpriteGfxAddress, SpriteGfxSize)); + AnimatedPixelData = GfxToPixelMap( + rom.ReadBytes(pointers.AnimatedGfxAddress, AnimatedGfxSize)); + MarioPixelData = GfxToPixelMap( + rom.ReadBytes(pointers.MarioGfxAddress, PlayerGfxSize)); + LuigiPixelData = GfxToPixelMap( + rom.ReadBytes(pointers.LuigiGfxAddress, PlayerGfxSize)); + MenuPixelData = Gfx2BppToPixelMap( + rom.ReadBytes(pointers.MenuGfxAddress, MenuGfxSize)); + + BonusAreaTileSetTable = rom.ReadBytesIndirect( + pointers.BonusAreaTileSetTablePointer, + count: 2); + + if (BonusAreaTileSetTable.Any(index => index >= TilesetCount)) + { + throw new ArgumentException("GFX Data is invalid"); + } + + TileSetDestIndexTable = rom.ReadInt16ArrayIndirectAs( + pointers.TileSetDestIndexTablePointer, + TilesetCount, + x => (ushort)x); + + if (TileSetDestIndexTable.Skip(1).Any(index => index < 0x1000)) + { + throw new ArgumentException("GFX data is invalid!"); + } + + var banks = rom.ReadInt16ArrayIndirectAs( + pointers.TileSetAddressBankByteTablePointer, + TilesetCount, + x => (ushort)x); + var words = rom.ReadInt16ArrayIndirectAs( + pointers.TileSetAddressWordTablePointer, + TilesetCount, + x => (ushort)x); + var sizes = rom.ReadInt16ArrayIndirectAs( + pointers.TileSetSizeTablePointer, + TilesetCount, + x => (ushort)x); + + TileSetTable = new byte[TilesetCount][]; + for (var i = 1; i < TileSetTable.Length; i++) + { + var address = (banks[i] << 0x10) | words[i]; + TileSetTable[i] = GfxToPixelMap(rom.ReadBytes(address, sizes[i])); + } + + var maxIndexes = TileSetTable.Skip(1).Zip( + TileSetDestIndexTable.Skip(1), + (tiles, index) => ((index - 0x1000) << 2) + tiles.Length); + + // Next we must ensure that every tileset will fit into the pixel data array. + if (maxIndexes.Any(index => index >= TotalPixelDataSize)) + { + throw new ArgumentException("GFX data is invalid!"); + } + + TileSetActions = new Func>?[0x20] + { + null, + GetUndergroundTileSets, + GetGrassTileSets, + GetUndergroundTileSets, + GetBowserCastleTileSets, + GetGrassTileSets, + null, + GetStarryNightTileSets, + null, + GetStarryNightTileSets, + GetGameOverTileSets, + GetGrassTileSets, + GetGrassTileSets, + null, + GetGrassTileSets, + null, + GetGrassTileSets, + null, + null, + null, + null, + null, + null, + null, + GetUndergroundTileSets, + null, + null, + null, + null, + null, + null, + null, + }; + } + + private byte[] AreaPixelData + { + get; + } + + private byte[] SpritePixelData + { + get; + } + + private byte[] AnimatedPixelData + { + get; + } + + private byte[] MarioPixelData + { + get; + } + + private byte[] LuigiPixelData + { + get; + } + + private byte[] MenuPixelData + { + get; + } + + private byte[][] TileSetTable + { + get; + } + + private int[] TileSetDestIndexTable + { + get; + } + + private byte[] BonusAreaTileSetTable + { + get; + } + + private Func>?[] TileSetActions + { + get; + } + + public void ReadStaticData(Span dest) + { + if (dest.Length < TotalPixelDataSize) + { + throw new ArgumentException( + "Destination array is of insufficient size.", + nameof(dest)); + } + + AreaPixelData.CopyTo(dest); + SpritePixelData.CopyTo(dest[SpritePixelDataStartIndex..]); + AnimatedPixelData.CopyTo(dest[AnimatedPixelDataStartIndex..]); + MarioPixelData.CopyTo(dest[MarioPixelDataStartIndex..]); + LuigiPixelData.CopyTo(dest[LuigiPixelDataStartIndex..]); + MenuPixelData.CopyTo(dest[MenuPixelDataStartIndex..]); + } + + public void ReadAnimationFrame(int frame, Span pixelData) + { + var actualFrame = (frame >> 3) & 7; + var src = new Span( + AnimatedPixelData, + AnimatedPixelDataFrameOffset * actualFrame, + AnimatedPixelDataFrameSize); + var dest = pixelData.Slice( + AnimatedPixelDataDestIndex, + AnimatedPixelDataFrameSize); + src.CopyTo(dest); + } + + public void ReadAreaTileSet( + int areaIndex, + int areaTileSetIndex, + Player player, + Span pixelData) + { + if (areaTileSetIndex == 1) + { + areaTileSetIndex = BonusAreaTileSetTable[(int)player]; + } + + ReadTileSet(areaTileSetIndex, pixelData); + + // HACK: These levels don't load a complete tileset. It uses tile sets of the + // area before them. Eventually, I'll need to devise a system to better load + // tile sets. + if (areaIndex == 2) + { + ReadTileSet(4, pixelData); + } + else if (areaIndex == 0x0F) + { + ReadTileSet(0x17, pixelData); + } + + var action = TileSetActions[areaTileSetIndex & 0x1F]; + if (action is not null) + { + foreach (var tileset in action(areaIndex)) + { + ReadTileSet(tileset, pixelData); + } + } + } + + public void ReadTileSet(int tileSetIndex, Span pixelData) + { + var tileSet = TileSetTable[tileSetIndex]; + var destIndex = (TileSetDestIndexTable[tileSetIndex] - 0x1000) << 2; + tileSet.CopyTo(pixelData.Slice(destIndex, tileSet.Length)); + } + + public void WriteToGameData(Rom rom, GfxDataPointers pointers) + { + rom.WriteArrayAsInt16Indirect( + pointers.TileSetDestIndexTablePointer, + TileSetDestIndexTable, + x => (short)x); + + for (var i = 1; i < TileSetTable.Length; i++) + { + var bank = (ushort)rom.ReadInt16IndirectIndexed( + pointers.TileSetAddressBankByteTablePointer, + i); + var word = (ushort)rom.ReadInt16IndirectIndexed( + pointers.TileSetAddressWordTablePointer, + i); + var src = (bank << 0x10) | word; + rom.WriteBytes(src, PixelMapToGfx(TileSetTable[i])); + } + + rom.WriteBytesIndirect( + pointers.BonusAreaTileSetTablePointer, + BonusAreaTileSetTable); + + rom.WriteBytes( + pointers.MenuGfxAddress, + PixelMapToGfx2Bpp(MenuPixelData)); + rom.WriteBytes( + pointers.LuigiGfxAddress, + PixelMapToGfx(LuigiPixelData)); + rom.WriteBytes( + pointers.MarioGfxAddress, + PixelMapToGfx(MarioPixelData)); + rom.WriteBytes( + pointers.AnimatedGfxAddress, + PixelMapToGfx(AnimatedPixelData)); + rom.WriteBytes( + pointers.SpriteGfxAddress, + PixelMapToGfx(SpritePixelData)); + rom.WriteBytes( + pointers.AreaGfxAddress, + PixelMapToGfx(AreaPixelData)); + } + + private IEnumerable GetGrassTileSets(int areaIndex) + { + yield return areaIndex is 0x16 or 0x14 or 0x0D + ? 0x12 + : 0x17; + } + + private IEnumerable GetUndergroundTileSets(int areaIndex) + { + yield return 0x11; + } + + private IEnumerable GetStarryNightTileSets(int areaIndex) + { + yield return 0x16; + yield return 0x12; + } + + private IEnumerable GetBowserCastleTileSets(int areaIndex) + { + yield return 0x13; + yield return 0x14; + } + + private IEnumerable GetGameOverTileSets(int areaIndex) + { + yield return 0x15; + } +} diff --git a/src/Smas/Smb1/GfxDataPointers.cs b/src/Smas/Smb1/GfxDataPointers.cs new file mode 100644 index 0000000..f39e433 --- /dev/null +++ b/src/Smas/Smb1/GfxDataPointers.cs @@ -0,0 +1,113 @@ +// +// 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; + +public class GfxDataPointers +{ + public static readonly GfxDataPointers Jp10 = new( + baseAddress: 0x05E6BA); + + public static readonly GfxDataPointers Jp11 = new( + baseAddress: 0x05E70B); + + public static readonly GfxDataPointers Usa = new( + baseAddress: 0x05E6C3); + + public static readonly GfxDataPointers UsaPlusW = new( + baseAddress: 0x05E714); + + public static readonly GfxDataPointers Eu = new( + baseAddress: 0x05E714); + + public static readonly GfxDataPointers EuPlusW = new( + baseAddress: 0x05E714); + + public static readonly GfxDataPointers UsaSmb1 = new( + gfxBaseAddress: 0x038000, + baseAddress: 0x02E6C6); + + public GfxDataPointers( + int bonusAreaTileSetTablePointer, + int tileSetAddressBankByteTablePointer, + int tileSetAddressWordTablePointer, + int tileSetDestIndexTablePointer, + int tileSetSizeTablePointer, + int gfxBaseAddress = 0x68000) + : this( + areaGfxAddress: gfxBaseAddress, + spriteGfxAddress: gfxBaseAddress + 0x10000, + animatedGfxAddress: gfxBaseAddress + 0x4000, + marioGfxAddress: gfxBaseAddress + 0x40000, + luigiGfxAddress: gfxBaseAddress + 0x44000, + menuGfxAddress: gfxBaseAddress + 0x67800, + bonusAreaTileSetTablePointer, + tileSetAddressBankByteTablePointer, + tileSetAddressWordTablePointer, + tileSetDestIndexTablePointer, + tileSetSizeTablePointer) + { } + + public GfxDataPointers( + int areaGfxAddress, + int spriteGfxAddress, + int animatedGfxAddress, + int marioGfxAddress, + int luigiGfxAddress, + int menuGfxAddress, + int bonusAreaTileSetTablePointer, + int tileSetAddressBankByteTablePointer, + int tileSetAddressWordTablePointer, + int tileSetDestIndexTablePointer, + int tileSetSizeTablePointer) + { + AreaGfxAddress = areaGfxAddress; + SpriteGfxAddress = spriteGfxAddress; + AnimatedGfxAddress = animatedGfxAddress; + MarioGfxAddress = marioGfxAddress; + LuigiGfxAddress = luigiGfxAddress; + MenuGfxAddress = menuGfxAddress; + BonusAreaTileSetTablePointer = bonusAreaTileSetTablePointer; + TileSetAddressBankByteTablePointer = tileSetAddressBankByteTablePointer; + TileSetAddressWordTablePointer = tileSetAddressWordTablePointer; + TileSetDestIndexTablePointer = tileSetDestIndexTablePointer; + TileSetSizeTablePointer = tileSetSizeTablePointer; + } + + private GfxDataPointers( + int baseAddress, + int gfxBaseAddress = 0x68000) + : this( + bonusAreaTileSetTablePointer: baseAddress, + tileSetAddressBankByteTablePointer: baseAddress + 0x16A, + tileSetAddressWordTablePointer: baseAddress + 0x172, + tileSetDestIndexTablePointer: baseAddress + 0x178, + tileSetSizeTablePointer: baseAddress + 0x17E, + gfxBaseAddress: gfxBaseAddress) + { } + + public int AreaGfxAddress { get; } + + public int SpriteGfxAddress { get; } + + public int AnimatedGfxAddress { get; } + + public int MarioGfxAddress { get; } + + public int LuigiGfxAddress { get; } + + public int MenuGfxAddress { get; } + + public int BonusAreaTileSetTablePointer { get; } + + public int TileSetAddressBankByteTablePointer { get; } + + public int TileSetAddressWordTablePointer { get; } + + public int TileSetDestIndexTablePointer { get; } + + public int TileSetSizeTablePointer { get; } +} diff --git a/src/Smas/Smb1/Map16Data.cs b/src/Smas/Smb1/Map16Data.cs new file mode 100644 index 0000000..2b50017 --- /dev/null +++ b/src/Smas/Smb1/Map16Data.cs @@ -0,0 +1,110 @@ +// +// 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; + +using System; +using System.Collections.ObjectModel; + +using Snes; + +public class Map16Data +{ + public Map16Data(Rom rom, Map16DataPointers pointers) + { + Tiles = new ReadOnlyCollection( + new Obj16Tile[][] + { + new Obj16Tile[0x2B], + new Obj16Tile[0x38], + new Obj16Tile[0x0E], + new Obj16Tile[0x3E], + } + ); + + var isTileAccessible = new bool[0x100]; + for (var i = 0; i < Tiles.Count; i++) + { + var startIndex = i << 6; + for (var j = 0; j < Tiles[i].Length; j++) + { + isTileAccessible[startIndex + j] = true; + } + } + + IsTileAccessible = new ReadOnlyCollection(isTileAccessible); + + var bank = pointers.LowBytePointer & 0xFF0000; + var lows = rom.ReadBytesIndirect( + pointers.LowBytePointer, Tiles.Count); + var highs = rom.ReadBytesIndirect( + pointers.HighBytePointer, Tiles.Count); + + for (var i = 0; i < Tiles.Count; i++) + { + unsafe + { + fixed (Obj16Tile* ptr = Tiles[i]) + { + var dest = new Span( + (short*)ptr, + Tiles[i].Length * Obj16Tile.NumberOfTiles); + var address = bank | (highs[i] << 8) | lows[i]; + rom.ReadInt16Array(address, dest); + } + } + } + } + + public ReadOnlyCollection IsTileAccessible + { + get; + } + + private ReadOnlyCollection Tiles + { + get; + } + + public void ReadStaticTiles(Span dest) + { + for (var i = 0; i < Tiles.Count; i++) + { + var destSlice = dest.Slice(i << 6, 0x40); + Tiles[i].CopyTo(destSlice); + } + } + + public void WriteTiles(Span source) + { + for (var i = 0; i < Tiles.Count; i++) + { + var sourceSlice = source.Slice(i << 6, 0x40); + sourceSlice.CopyTo(Tiles[i]); + } + } + + public void WriteToGameData(Rom rom, Map16DataPointers pointers) + { + var bank = pointers.LowBytePointer & 0xFF0000; + var lows = rom.ReadBytesIndirect(pointers.LowBytePointer, Tiles.Count); + var highs = rom.ReadBytesIndirect(pointers.HighBytePointer, Tiles.Count); + for (var i = 0; i < Tiles.Count; i++) + { + unsafe + { + fixed (Obj16Tile* ptr = Tiles[i]) + { + var src = new Span( + (short*)ptr, + Tiles[i].Length * Obj16Tile.NumberOfTiles); + var address = bank | (highs[i] << 8) | lows[i]; + rom.WriteInt16Array(address, src); + } + } + } + } +} diff --git a/src/Smas/Smb1/Map16DataPointers.cs b/src/Smas/Smb1/Map16DataPointers.cs new file mode 100644 index 0000000..b0c1bc8 --- /dev/null +++ b/src/Smas/Smb1/Map16DataPointers.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 Maseya.Smas.Smb1; + +public class Map16DataPointers +{ + public static readonly Map16DataPointers Jp10 = new( + baseAddress: 0x039298); + + public static readonly Map16DataPointers Jp11 = new( + baseAddress: 0x0392A5); + + public static readonly Map16DataPointers Usa = new( + baseAddress: 0x03927D); + + public static readonly Map16DataPointers UsaPlusW = new( + baseAddress: 0x03928A); + + public static readonly Map16DataPointers Eu = new( + baseAddress: 0x03929B); + + public static readonly Map16DataPointers EuPlusW = new( + baseAddress: 0x03929B); + + public static readonly Map16DataPointers UsaSmb1 = new( + baseAddress: 0x0092BC); + + public Map16DataPointers(int lowBytePointer, int highBytePointer) + { + LowBytePointer = lowBytePointer; + HighBytePointer = highBytePointer; + } + + private Map16DataPointers(int baseAddress) + : this( + lowBytePointer: baseAddress, + highBytePointer: baseAddress + 0x05) + { } + + public int LowBytePointer + { + get; + } + + public int HighBytePointer + { + get; + } +} diff --git a/src/Smas/Smb1/PaletteData.cs b/src/Smas/Smb1/PaletteData.cs new file mode 100644 index 0000000..bfff438 --- /dev/null +++ b/src/Smas/Smb1/PaletteData.cs @@ -0,0 +1,216 @@ +// +// 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; + +using System; +using System.Linq; + +using Snes; + +public class PaletteData +{ + public const int ColorsPerRow = 0x10; + public const int RowsPerPalette = 0x10; + public const int TotalPaletteSize = ColorsPerRow * RowsPerPalette; + + private const int RowIndexTableSize = 0x220; + private const int IndexTableSize = 0x42; + private const int ColorTableSize = 0x3E0; + private const int BonusAreaRowIndex = 7; + private const int LuigiBonusAreaRowCount = 1; + private const int LuigiBonusAreaColorTableSize = ColorsPerRow * LuigiBonusAreaRowCount; + private const int PlayerPaletteRowIndex = 0x0F; + private const int PlayerPaletteRowCount = 4; + private const int PlayerPaletteTableSize = ColorsPerRow * PlayerPaletteRowCount; + + public PaletteData(Rom rom, PaletteDataPointers pointers) + { + RowIndexTable = rom.ReadBytesIndirect( + pointers.RowIndexTablePointer, + RowIndexTableSize); + + // Make sure the index tables stay within the bounds of the tables they index into. + if (RowIndexTable.Any(rowIndex => rowIndex >= IndexTableSize)) + { + throw new ArgumentException( + "Element in palette row index table attempts to access a value outside of the index table."); + } + + IndexTable = rom.ReadInt16ArrayIndirectAs( + pointers.IndexTablePointer, + IndexTableSize, + x => x >> 1); + + if (IndexTable.Any(index => index > ColorTableSize - ColorsPerRow)) + { + throw new ArgumentException( + "Element in palette index table attempts to access a value outside of the color table."); + } + + ColorTable = rom.ReadInt16ArrayIndirectAs( + pointers.ColorTablePointer, + ColorTableSize, + x => Color32BppArgb.FromSnesColor(x)); + LuigiBonusAreaColorTable = rom.ReadInt16ArrayIndirectAs( + pointers.LuigiBonusAreaColorTablePointer, + LuigiBonusAreaColorTableSize, + x => Color32BppArgb.FromSnesColor(x)); + PlayerPaletteTable = rom.ReadInt16ArrayIndirectAs( + pointers.PlayerPaletteTablePointer, + PlayerPaletteTableSize, + x => Color32BppArgb.FromSnesColor(x)); + } + + private byte[] RowIndexTable + { + get; + } + + private int[] IndexTable + { + get; + } + + private Color32BppArgb[] ColorTable + { + get; + } + + private Color32BppArgb[] LuigiBonusAreaColorTable + { + get; + } + + private Color32BppArgb[] PlayerPaletteTable + { + get; + } + + public void ReadPalette(int paletteIndex, Span dest) + { + ReadPalette( + paletteIndex, + isLuigiBonusArea: false, + player: Player.Mario, + state: PlayerState.Small, + dest); + } + + public void ReadPalette( + int paletteIndex, + bool isLuigiBonusArea, + Player player, + PlayerState state, + Span dest) + { + var sourceRowStartIndex = paletteIndex * RowsPerPalette; + for (var destRowIndex = 0; destRowIndex < RowsPerPalette; destRowIndex++) + { + var sourceRowIndex = RowIndexTable[sourceRowStartIndex + destRowIndex]; + var sourceIndex = IndexTable[sourceRowIndex]; + var sourceRow = new Span( + ColorTable, + sourceIndex, + ColorsPerRow); + + var destColorIndex = destRowIndex * ColorsPerRow; + var destRow = dest.Slice(destColorIndex, ColorsPerRow); + + sourceRow.CopyTo(destRow); + } + + if (isLuigiBonusArea) + { + var bonusAreaIndex = BonusAreaRowIndex * ColorsPerRow; + var bonusAreaRow = dest.Slice(bonusAreaIndex, ColorsPerRow); + LuigiBonusAreaColorTable.CopyTo(bonusAreaRow); + } + + var playerPaletteSourceIndex = 0; + if (player == Player.Luigi) + { + playerPaletteSourceIndex |= 0x10; + } + + if (state == PlayerState.Fire) + { + playerPaletteSourceIndex |= 0x20; + } + + var playerPaletteSourceRow = new Span( + PlayerPaletteTable, + playerPaletteSourceIndex, + ColorsPerRow); + var playerPaletteDestRow = dest.Slice(0xF0, ColorsPerRow); + playerPaletteSourceRow.CopyTo(playerPaletteDestRow); + } + + public void ReadPlayerPalettes(Span dest) + { + PlayerPaletteTable.CopyTo(dest); + } + + public void WritePlayerPalettes(Span src) + { + src.CopyTo(PlayerPaletteTable); + } + + public void WritePalette( + Span source, + int paletteIndex, + bool isLuigiBonusArea = false) + { + var destRowStartIndex = paletteIndex * RowsPerPalette; + for (var sourceRowIndex = 0; sourceRowIndex < RowsPerPalette; sourceRowIndex++) + { + var sourceIndex = sourceRowIndex * ColorsPerRow; + var sourceRow = source.Slice(sourceIndex, ColorsPerRow); + if (isLuigiBonusArea && sourceRowIndex == BonusAreaRowIndex) + { + sourceRow.CopyTo(LuigiBonusAreaColorTable); + } + else if (sourceRowIndex == PlayerPaletteRowIndex) + { + // TODO(nrg): Player palette + } + else + { + var destRowIndex = RowIndexTable[destRowStartIndex + sourceRowIndex]; + var destIndex = IndexTable[destRowIndex]; + var destRow = new Span( + ColorTable, + destIndex, + ColorsPerRow); + + sourceRow.CopyTo(destRow); + } + } + } + + public void WriteToGameData(Rom rom, PaletteDataPointers pointers) + { + rom.WriteArrayAsInt16Indirect( + pointers.PlayerPaletteTablePointer, + PlayerPaletteTable, + x => (short)Color32BppArgb.ToSnesColor(x)); + rom.WriteArrayAsInt16Indirect( + pointers.LuigiBonusAreaColorTablePointer, + LuigiBonusAreaColorTable, + x => (short)Color32BppArgb.ToSnesColor(x)); + rom.WriteArrayAsInt16Indirect( + pointers.ColorTablePointer, + ColorTable, + x => (short)Color32BppArgb.ToSnesColor(x)); + rom.WriteArrayAsInt16Indirect( + pointers.IndexTablePointer, + IndexTable, + x => (short)(x << 1)); + rom.WriteBytesIndirect( + pointers.RowIndexTablePointer, + RowIndexTable); + } +} diff --git a/src/Smas/Smb1/PaletteDataPointers.cs b/src/Smas/Smb1/PaletteDataPointers.cs new file mode 100644 index 0000000..78f7dd4 --- /dev/null +++ b/src/Smas/Smb1/PaletteDataPointers.cs @@ -0,0 +1,87 @@ +// +// 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; + +public class PaletteDataPointers +{ + public static readonly PaletteDataPointers Jp10 = new( + baseAddress: 0x049630); + + public static readonly PaletteDataPointers Jp11 = new( + baseAddress: 0x049630); + + public static readonly PaletteDataPointers Usa = new( + baseAddress: 0x04961D); + + public static readonly PaletteDataPointers UsaPlusW = new( + baseAddress: 0x04961D); + + public static readonly PaletteDataPointers Eu = new( + baseAddress: 0x049643); + + public static readonly PaletteDataPointers EuPlusW = new( + baseAddress: 0x049643); + + public static readonly PaletteDataPointers UsaSmb1 = new( + baseAddress: 0x01961D); + + public PaletteDataPointers( + int rowIndexTablePointer, + int indexTablePointer, + int colorTablePointer, + int luigiBonusAreaColorTablePointer, + int playerPaletteTablePointer) + { + RowIndexTablePointer = rowIndexTablePointer; + IndexTablePointer = indexTablePointer; + ColorTablePointer = colorTablePointer; + LuigiBonusAreaColorTablePointer = luigiBonusAreaColorTablePointer; + PlayerPaletteTablePointer = playerPaletteTablePointer; + } + + private PaletteDataPointers(int baseAddress) + : this( + baseAddress, + playerPaletteTablePointer: baseAddress + 0x4FD) + { + } + + private PaletteDataPointers(int baseAddress, int playerPaletteTablePointer) + : this( + rowIndexTablePointer: baseAddress, + indexTablePointer: baseAddress + 0x0B, + colorTablePointer: baseAddress + 0x16, + luigiBonusAreaColorTablePointer: baseAddress + 0x4C, + playerPaletteTablePointer) + { + } + + public int RowIndexTablePointer + { + get; + } + + public int IndexTablePointer + { + get; + } + + public int ColorTablePointer + { + get; + } + + public int LuigiBonusAreaColorTablePointer + { + get; + } + + public int PlayerPaletteTablePointer + { + get; + } +} diff --git a/src/Smas/Smb1/Player.cs b/src/Smas/Smb1/Player.cs new file mode 100644 index 0000000..136ee8b --- /dev/null +++ b/src/Smas/Smb1/Player.cs @@ -0,0 +1,13 @@ +// +// 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; + +public enum Player +{ + Mario, + Luigi +} diff --git a/src/Smas/Smb1/PlayerState.cs b/src/Smas/Smb1/PlayerState.cs new file mode 100644 index 0000000..d491955 --- /dev/null +++ b/src/Smas/Smb1/PlayerState.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 Maseya.Smas.Smb1; + +public enum PlayerState +{ + Small = 0, + Big, + Fire, +} diff --git a/src/Smas/Smb1/Pointers.cs b/src/Smas/Smb1/Pointers.cs new file mode 100644 index 0000000..06952ff --- /dev/null +++ b/src/Smas/Smb1/Pointers.cs @@ -0,0 +1,160 @@ +// +// 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; + +using System; + +using AreaData; +using AreaData.ObjectData; + +using Snes; + +public class Pointers +{ + public static readonly Pointers Jp10 = new( + PaletteDataPointers.Jp10, + GfxDataPointers.Jp10, + Map16DataPointers.Jp10, + TilemapLoaderPointers.Jp10, + AreaLoaderPointers.Jp10, + AreaObjectRendererPointers.Jp10); + + public static readonly Pointers Jp11 = new( + PaletteDataPointers.Jp11, + GfxDataPointers.Jp11, + Map16DataPointers.Jp11, + TilemapLoaderPointers.Jp11, + AreaLoaderPointers.Jp11, + AreaObjectRendererPointers.Jp11); + + public static readonly Pointers Usa = new( + PaletteDataPointers.Usa, + GfxDataPointers.Usa, + Map16DataPointers.Usa, + TilemapLoaderPointers.Usa, + AreaLoaderPointers.Usa, + AreaObjectRendererPointers.Usa); + + public static readonly Pointers UsaPlusW = new( + PaletteDataPointers.UsaPlusW, + GfxDataPointers.UsaPlusW, + Map16DataPointers.UsaPlusW, + TilemapLoaderPointers.UsaPlusW, + AreaLoaderPointers.UsaPlusW, + AreaObjectRendererPointers.UsaPlusW); + + public static readonly Pointers Eu = new( + PaletteDataPointers.Eu, + GfxDataPointers.Eu, + Map16DataPointers.Eu, + TilemapLoaderPointers.Eu, + AreaLoaderPointers.Eu, + AreaObjectRendererPointers.Eu); + + public static readonly Pointers EuPlusW = new( + PaletteDataPointers.EuPlusW, + GfxDataPointers.EuPlusW, + Map16DataPointers.EuPlusW, + TilemapLoaderPointers.EuPlusW, + AreaLoaderPointers.EuPlusW, + AreaObjectRendererPointers.EuPlusW); + + public static readonly Pointers UsaSmb1 = new( + PaletteDataPointers.UsaSmb1, + GfxDataPointers.UsaSmb1, + Map16DataPointers.UsaSmb1, + TilemapLoaderPointers.UsaSmb1, + AreaLoaderPointers.UsaSmb1, + AreaObjectRendererPointers.UsaSmb1); + + private Pointers( + PaletteDataPointers paletteDataPointers, + GfxDataPointers gfxDataPointers, + Map16DataPointers map16DataPointers, + TilemapLoaderPointers tilemapLoaderPointers, + AreaLoaderPointers areaLoaderPointers, + AreaObjectRendererPointers areaObjectRendererPointers) + { + PaletteDataPointers = paletteDataPointers; + GfxDataPointers = gfxDataPointers; + Map16DataPointers = map16DataPointers; + TilemapLoaderPointers = tilemapLoaderPointers; + AreaLoaderPointers = areaLoaderPointers; + AreaObjectRendererPointers = areaObjectRendererPointers; + } + + public PaletteDataPointers PaletteDataPointers + { + get; + } + + public GfxDataPointers GfxDataPointers + { + get; + } + + public Map16DataPointers Map16DataPointers + { + get; + } + + public TilemapLoaderPointers TilemapLoaderPointers + { + get; + } + + public AreaLoaderPointers AreaLoaderPointers + { + get; + } + + public AreaObjectRendererPointers AreaObjectRendererPointers + { + get; + } + + public static Pointers GetPointers(Rom rom) + { + // TODO(nrg): This will need to be customized more later. + switch (rom.DestinationCode) + { + case DestinationCode.NorthAmerica: + if (rom.GameTitle.Contains("WORLD")) + { + return UsaPlusW; + } + + if (rom.GameTitle == "Super Mario Bros. 1") + { + return UsaSmb1; + } + + return Usa; + + case DestinationCode.Japan: + if (rom.MaskRomVersion == 1) + { + return Jp11; + } + + return Jp10; + + case DestinationCode.Europe: + if (rom.GameTitle.Contains("WORLD")) + { + return EuPlusW; + } + + return Eu; + + default: + throw new ArgumentException( + "Could not determine which type of All-Stars ROM was loaded.", + nameof(rom)); + } + } +} diff --git a/src/Smas/Smb1/Resources.Designer.cs b/src/Smas/Smb1/Resources.Designer.cs new file mode 100644 index 0000000..bed6865 --- /dev/null +++ b/src/Smas/Smb1/Resources.Designer.cs @@ -0,0 +1,1107 @@ +//------------------------------------------------------------------------------ +// +// 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 Maseya.Smas.Smb1 { + 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("Maseya.Smas.Smb1.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 string similar to Transition Command. + /// + internal static string AreaPointer { + get { + return ResourceManager.GetString("AreaPointer", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Area-specific platform. + /// + internal static string AreaSpecificPlatform { + get { + return ResourceManager.GetString("AreaSpecificPlatform", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Bullet Bill Shooter. + /// + internal static string AreaSpecificPlatform_BulletBillTurrets { + get { + return ResourceManager.GetString("AreaSpecificPlatform_BulletBillTurrets", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Cloud Ground. + /// + internal static string AreaSpecificPlatform_CloudGround { + get { + return ResourceManager.GetString("AreaSpecificPlatform_CloudGround", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Mushroom Platform. + /// + internal static string AreaSpecificPlatform_Mushrooms { + get { + return ResourceManager.GetString("AreaSpecificPlatform_Mushrooms", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Tree Top Platform. + /// + internal static string AreaSpecificPlatform_Trees { + get { + return ResourceManager.GetString("AreaSpecificPlatform_Trees", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Pulley Platforms. + /// + internal static string BalanceHorizontalRope { + get { + return ResourceManager.GetString("BalanceHorizontalRope", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Rope for Lift Balance. + /// + internal static string BalanceRopeLift { + get { + return ResourceManager.GetString("BalanceRopeLift", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Beanstalk. + /// + internal static string Beanstalk { + get { + return ResourceManager.GetString("Beanstalk", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Squid. + /// + internal static string Blooper { + get { + return ResourceManager.GetString("Blooper", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Bowser: King of the Koopa. + /// + internal static string Bowser { + get { + return ResourceManager.GetString("Bowser", 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 Bowser's Fire. + /// + internal static string BowserFire { + get { + return ResourceManager.GetString("BowserFire", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Brick. + /// + internal static string Brick { + get { + return ResourceManager.GetString("Brick", 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 Rope Bridge. + /// + internal static string Bridge { + get { + return ResourceManager.GetString("Bridge", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Single Bullet Bill. + /// + internal static string BulletBill { + get { + return ResourceManager.GetString("BulletBill", 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 Bullet Bill or Cheep-Cheeps. + /// + internal static string BulletBillOrCheepCheep { + get { + return ResourceManager.GetString("BulletBillOrCheepCheep", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Buzzy Beetle. + /// + internal static string BuzzyBeetle { + get { + return ResourceManager.GetString("BuzzyBeetle", 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 string similar to Cheep-Cheep. + /// + internal static string CheepCheep { + get { + return ResourceManager.GetString("CheepCheep", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Clockwise. + /// + internal static string Clockwise { + get { + return ResourceManager.GetString("Clockwise", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Coin. + /// + internal static string Coin { + get { + return ResourceManager.GetString("Coin", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Counter-Clockwise. + /// + internal static string CounterClockwise { + get { + return ResourceManager.GetString("CounterClockwise", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Down. + /// + internal static string Down { + get { + return ResourceManager.GetString("Down", resourceCulture); + } + } + + /// + /// 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 Empty Tile. + /// + internal static string EmptyTile { + get { + return ResourceManager.GetString("EmptyTile", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Enterable Pipe. + /// + internal static string EnterablePipe { + get { + return ResourceManager.GetString("EnterablePipe", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Extendable J-Pipe. + /// + internal static string ExtendableJPipe { + get { + return ResourceManager.GetString("ExtendableJPipe", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Falling. + /// + internal static string Falling { + get { + return ResourceManager.GetString("Falling", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Fast. + /// + internal static string Fast { + get { + return ResourceManager.GetString("Fast", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Fire Bar. + /// + internal static string FireBar { + get { + return ResourceManager.GetString("FireBar", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Single Firework. + /// + internal static string Firework { + get { + return ResourceManager.GetString("Firework", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Flag Pole. + /// + internal static string FlagPole { + get { + return ResourceManager.GetString("FlagPole", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Flies Horizontally. + /// + internal static string FliesHorizontally { + get { + return ResourceManager.GetString("FliesHorizontally", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Flies in place. + /// + internal static string FliesInPlace { + get { + return ResourceManager.GetString("FliesInPlace", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Flies Vertically. + /// + internal static string FliesVertically { + get { + return ResourceManager.GetString("FliesVertically", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Flying. + /// + internal static string Flying { + get { + return ResourceManager.GetString("Flying", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Foreground Change. + /// + internal static string ForegroundChange { + get { + return ResourceManager.GetString("ForegroundChange", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Generator. + /// + internal static string Generator { + get { + return ResourceManager.GetString("Generator", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Goomba. + /// + internal static string Goomba { + get { + return ResourceManager.GetString("Goomba", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Two Goombas. + /// + internal static string Goomba2 { + get { + return ResourceManager.GetString("Goomba2", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Three Goombas. + /// + internal static string Goomba3 { + get { + return ResourceManager.GetString("Goomba3", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Green. + /// + internal static string Green { + get { + return ResourceManager.GetString("Green", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Two Green Koopa Troopas. + /// + internal static string GreenKoopaTroopa2 { + get { + return ResourceManager.GetString("GreenKoopaTroopa2", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Three Green Koopa Troopas. + /// + internal static string GreenKoopaTroopa3 { + get { + return ResourceManager.GetString("GreenKoopaTroopa3", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Hammer Bros.. + /// + internal static string HammerBros { + get { + return ResourceManager.GetString("HammerBros", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to HiddenBlock. + /// + internal static string HiddenBlock { + get { + return ResourceManager.GetString("HiddenBlock", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Hole. + /// + internal static string Hole { + get { + return ResourceManager.GetString("Hole", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Hole with water or lava. + /// + internal static string HoleWithWaterOrLava { + get { + return ResourceManager.GetString("HoleWithWaterOrLava", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Horizontal Bricks. + /// + internal static string HorizontalBricks { + get { + return ResourceManager.GetString("HorizontalBricks", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Horizontal Coins. + /// + internal static string HorizontalCoins { + get { + return ResourceManager.GetString("HorizontalCoins", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Width={0}. + /// + internal static string HorizontallyExtendableObject { + get { + return ResourceManager.GetString("HorizontallyExtendableObject", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Row of Coin Blocks. + /// + internal static string HorizontalQuestionBlocks { + get { + return ResourceManager.GetString("HorizontalQuestionBlocks", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Horizontal Blocks. + /// + 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 Koopa Paratroopa. + /// + internal static string KoopaParatroopa { + get { + return ResourceManager.GetString("KoopaParatroopa", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Koopa Troopa. + /// + internal static string KoopaTroopa { + get { + return ResourceManager.GetString("KoopaTroopa", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Lakitu. + /// + internal static string Lakitu { + get { + return ResourceManager.GetString("Lakitu", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Leaping. + /// + internal static string Leaping { + get { + return ResourceManager.GetString("Leaping", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Left. + /// + internal static string Left { + get { + return ResourceManager.GetString("Left", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to 1UP Mushroom. + /// + internal static string LifeMushroom { + get { + return ResourceManager.GetString("LifeMushroom", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Lift. + /// + internal static string Lift { + get { + return ResourceManager.GetString("Lift", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Long. + /// + internal static string Long { + get { + return ResourceManager.GetString("Long", 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 string similar to {0} ({1}). + /// + internal static string ObjectWithDescriptor { + get { + return ResourceManager.GetString("ObjectWithDescriptor", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to {0} ({1}; {2}). + /// + internal static string ObjectWithTwoDescriptors { + get { + return ResourceManager.GetString("ObjectWithTwoDescriptors", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Piranha Plant. + /// + internal static string PiranhaPlant { + get { + return ResourceManager.GetString("PiranhaPlant", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Podoboo. + /// + internal static string Podoboo { + get { + return ResourceManager.GetString("Podoboo", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Powerup. + /// + internal static string Powerup { + get { + return ResourceManager.GetString("Powerup", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Rope for pulley platforms. + /// + internal static string PulleyRope { + get { + return ResourceManager.GetString("PulleyRope", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Question Block. + /// + internal static string QuestionBlock { + get { + return ResourceManager.GetString("QuestionBlock", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Has random speed. + /// + internal static string RandomWalkSpeed { + get { + return ResourceManager.GetString("RandomWalkSpeed", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Red. + /// + internal static string Red { + get { + return ResourceManager.GetString("Red", 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 Right. + /// + internal static string Right { + get { + return ResourceManager.GetString("Right", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Rope for axe. + /// + internal static string RopeForAxe { + get { + return ResourceManager.GetString("RopeForAxe", 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 string similar to Screen Jump. + /// + 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 Skip to screen 0x{0:X2}. + /// + internal static string SetPage { + get { + return ResourceManager.GetString("SetPage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Short. + /// + internal static string Short { + get { + return ResourceManager.GetString("Short", 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 Spiny (Warning: Walk speed is random). + /// + internal static string Spiny { + get { + return ResourceManager.GetString("Spiny", 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. + /// + internal static string Staircase { + get { + return ResourceManager.GetString("Staircase", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Star. + /// + internal static string Star { + get { + return ResourceManager.GetString("Star", 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 string similar to 10 Coins. + /// + internal static string TenCoins { + get { + return ResourceManager.GetString("TenCoins", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Toad or Princess. + /// + internal static string ToadOrPrincess { + get { + return ResourceManager.GetString("ToadOrPrincess", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Unenterable Pipe. + /// + internal static string UnenterablePipe { + get { + return ResourceManager.GetString("UnenterablePipe", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Unknown command: {0}. + /// + internal static string UnknownCommand { + get { + return ResourceManager.GetString("UnknownCommand", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Up. + /// + internal static string Up { + get { + return ResourceManager.GetString("Up", 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. + /// + internal static string VerticalBalls { + get { + return ResourceManager.GetString("VerticalBalls", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Vertical Bricks. + /// + internal static string VerticalBricks { + get { + return ResourceManager.GetString("VerticalBricks", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Height={0}. + /// + internal static string VerticallyExtendableObject { + get { + return ResourceManager.GetString("VerticallyExtendableObject", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Vertical Sea Blocks. + /// + internal static string VerticalSeaBlocks { + get { + return ResourceManager.GetString("VerticalSeaBlocks", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Vertical Blocks. + /// + internal static string VerticalStones { + get { + return ResourceManager.GetString("VerticalStones", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Walks in place. + /// + internal static string WalksInPlace { + get { + return ResourceManager.GetString("WalksInPlace", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Walks off floors. + /// + internal static string WalksOffFloors { + get { + return ResourceManager.GetString("WalksOffFloors", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Warning: {0}. + /// + internal static string WarningDescriptor { + get { + return ResourceManager.GetString("WarningDescriptor", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Command: Load Warp Zone. + /// + internal static string WarpZoneCommand { + get { + return ResourceManager.GetString("WarpZoneCommand", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Y={0}. + /// + internal static string WithSpecificYCoord { + get { + return ResourceManager.GetString("WithSpecificYCoord", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Yellow. + /// + internal static string Yellow { + get { + return ResourceManager.GetString("Yellow", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Koopa Paratroopa (Yellow; Flies in place). + /// + internal static string YellowKoopaParatroopa { + get { + return ResourceManager.GetString("YellowKoopaParatroopa", resourceCulture); + } + } + } +} diff --git a/src/Smas/Smb1/Resources.resx b/src/Smas/Smb1/Resources.resx new file mode 100644 index 0000000..b006708 --- /dev/null +++ b/src/Smas/Smb1/Resources.resx @@ -0,0 +1,470 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 + + + Brick and Scenery Change + + + Mushroom Platform + + + Bullet Bill Shooter + + + Cloud Ground + + + Tree Top Platform + + + Pulley Platforms + + + Bowser Axe + + + Bowser Bridge + + + Rope Bridge + + + 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 + + + Empty Tile + + + Enterable Pipe + + + Extendable J-Pipe + + + Flag Pole + + + Foreground Change + + + Hole + + + Hole with water or lava + + + Horizontal Bricks + + + Horizontal Coins + + + Row of Coin Blocks + + + Horizontal Blocks + + + J-Pipe + + + Screen Loop Command + + + Rope for pulley platforms + + + Generator: Red flying cheep-cheeps + + + Rope for platform lifts + + + Screen Jump + + + Scroll Stop + + + Scroll Stop (Warp Zone) + + + Sideways Pipe + + + Spring Board + + + Staircase + + + Stop Generator (also stops Lakitus) + + + Unenterable Pipe + + + Unknown command: {0} + + + Used Block + + + Vertical Climbing Balls + + + Vertical Bricks + + + Vertical Sea Blocks + + + Vertical Blocks + + + Y={0} + + + {0} ({1}) + 0: Block name; 1: item name + + + Question Block + + + Brick + + + HiddenBlock + + + Powerup + + + 1UP Mushroom + + + 10 Coins + + + Star + + + Coin + + + Beanstalk + + + Area-specific platform + + + Rope for axe + + + Skip to screen 0x{0:X2} + + + Transition Command + + + Buzzy Beetle + + + Hammer Bros. + + + Squid + + + Single Bullet Bill + + + Koopa Paratroopa (Yellow; Flies in place) + + + Podoboo + + + Piranha Plant + + + Lakitu + + + Spiny (Warning: Walk speed is random) + + + {0} ({1}; {2}) + 0: Object: 1: first desctiption; 2: second description + + + Red + + + Green + + + Yellow + + + Koopa Troopa + + + Koopa Paratroopa + + + Lift + + + Walks off floors + + + Walks in place + + + Goomba + + + Flies in place + + + Warning: {0} + + + Cheep-Cheep + + + Leaping + + + Flies Vertically + + + Flies Horizontally + + + Has random speed + + + Flying + + + Height={0} + + + Width={0} + + + Generator + + + Bowser's Fire + + + Single Firework + + + Bullet Bill or Cheep-Cheeps + + + Fire Bar + + + Clockwise + + + Counter-Clockwise + + + Fast + + + Long + + + Rope for Lift Balance + + + Up + + + Down + + + Left + + + Right + + + Falling + + + Short + + + Bowser: King of the Koopa + + + Two Goombas + + + Three Goombas + + + Two Green Koopa Troopas + + + Three Green Koopa Troopas + + + Command: Load Warp Zone + + + Toad or Princess + + \ No newline at end of file diff --git a/src/Smas/Smb1/TilemapCommand.cs b/src/Smas/Smb1/TilemapCommand.cs new file mode 100644 index 0000000..ccaae3e --- /dev/null +++ b/src/Smas/Smb1/TilemapCommand.cs @@ -0,0 +1,124 @@ +// +// 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; + +using System; + +public struct TilemapCommand : IEquatable +{ + public const int TerminationValue = 0xE3F0; + + private ushort _value; + + private TilemapCommand(int value) + { + _value = (ushort)value; + } + + public int Command04 + { + get + { + return _value; + } + + set + { + _value = (ushort)value; + } + } + + public int CommandEF + { + get + { + return (Command04 & 0x3F0) >> 4; + } + + set + { + Command04 &= ~0x3F0; + Command04 |= (value << 4) & 0x3F0; + } + } + + public int CommandF1 + { + get + { + return Command04 & 0x0F; + } + + set + { + Command04 &= ~0x0F; + Command04 |= value & 0x0F; + } + } + + public int CommandED + { + get + { + return ((Command04 & 0xE000) | ((Command04 >> 1) & 0x0E00)) >> 8; + } + + set + { + Command04 &= ~0xEE00; + Command04 |= ((value & 0xE0) | ((value & 0x0E) << 1)) << 8; + } + } + + public bool IsTerminationCommand + { + get + { + return (CommandED & 0xF0) == 0xE0 && CommandEF == 0x3F; + } + } + + public static bool operator ==(TilemapCommand left, TilemapCommand right) + { + return left.Equals(right); + } + + public static bool operator !=(TilemapCommand left, TilemapCommand right) + { + return !(left == right); + } + + public static implicit operator int(TilemapCommand command) + { + return command.Command04; + } + + public static implicit operator TilemapCommand(int value) + { + return new TilemapCommand(value); + } + + public bool Equals(TilemapCommand other) + { + return Command04.Equals(other.Command04); + } + + public override bool Equals(object? obj) + { + return obj is TilemapCommand other && Equals(other); + } + + public override int GetHashCode() + { + return Command04; + } + + public override string ToString() + { + return Command04.ToString("X4"); + } +} diff --git a/src/Smas/Smb1/TilemapLoader.cs b/src/Smas/Smb1/TilemapLoader.cs new file mode 100644 index 0000000..5069505 --- /dev/null +++ b/src/Smas/Smb1/TilemapLoader.cs @@ -0,0 +1,168 @@ +// +// 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; + +using System; +using System.Collections.Generic; + +using Snes; + +public class TilemapLoader +{ + public TilemapLoader(Rom rom, TilemapLoaderPointers pointers, int numberOfAreas) + { + TilemapCommands = new TilemapCommand[numberOfAreas][]; + var indexes = rom.ReadInt16ArrayIndirectAs( + pointers.TilemapDataIndexPointer, + TilemapCommands.Length, + x => x >> 1); + + for (var i = 0; i < TilemapCommands.Length; i++) + { + var commands = new List(); + for (var j = 0; true; j++) + { + TilemapCommand command = rom.ReadInt16IndirectIndexed( + pointers.TilemapDataPointer, + indexes[i] + j); + if (command.IsTerminationCommand) + { + break; + } + + commands.Add(command); + } + + TilemapCommands[i] = commands.ToArray(); + } + + Layer2Tilemap = new int[0xD00 >> 1]; + BackgroundGenerationCommands = new Action[0x0D] + { + x => Layer2TilemapIndex++, + EnableHdmaGradient, + EnableHdmaWaving, + UnknownCommand03, + SetTilemapIndex, + FillTopAreaTilemap, + FillUndergroundRockPattern, + FillUnderwaterTopAreaTilemap, + FillWaterFallRockPattern, + x => EnableLayer3 = true, + GenerateWaterfallTiles, + SetSpecialTilemapIndex, + GenerateGoombaPillars, + }; + } + + public byte TileSetIndex + { + get; + private set; + } + + private int[] Layer2Tilemap + { + get; + } + + private bool EnableLayer3 + { + get; + set; + } + + private int Layer2TilemapIndex + { + get; + set; + } + + private TilemapCommand[][] TilemapCommands + { + get; + } + + private Action[] BackgroundGenerationCommands + { + get; + } + + public void LoadTilemap(int areaIndex) + { + EnableLayer3 = false; + Array.Clear(Layer2Tilemap, 0, Layer2Tilemap.Length); + foreach (var command in TilemapCommands[areaIndex]) + { + if ((command.CommandED & 0xF0) == 0xE0) + { + if (command.CommandEF == 0x3F) + { + Layer2Tilemap[++Layer2TilemapIndex] = 0xFFFF; + LoadTilemapTiles(); + } + else + { + BackgroundGenerationCommands[command.CommandEF](command); + } + } + else + { + } + } + } + + private void LoadTilemapTiles() + { + } + + private void EnableHdmaGradient(TilemapCommand command) + { + } + + private void EnableHdmaWaving(TilemapCommand command) + { + } + + private void UnknownCommand03(TilemapCommand command) + { + } + + private void SetTilemapIndex(TilemapCommand command) + { + TileSetIndex = (byte)command.CommandF1; + } + + private void FillTopAreaTilemap(TilemapCommand command) + { + } + + private void FillUndergroundRockPattern(TilemapCommand command) + { + } + + private void FillUnderwaterTopAreaTilemap(TilemapCommand command) + { + } + + private void FillWaterFallRockPattern(TilemapCommand command) + { + } + + private void GenerateWaterfallTiles(TilemapCommand command) + { + } + + private void SetSpecialTilemapIndex(TilemapCommand command) + { + Layer2TilemapIndex = command.CommandF1 | 0x10; + } + + private void GenerateGoombaPillars(TilemapCommand command) + { + } +} diff --git a/src/Smas/Smb1/TilemapLoaderPointers.cs b/src/Smas/Smb1/TilemapLoaderPointers.cs new file mode 100644 index 0000000..3b8e97a --- /dev/null +++ b/src/Smas/Smb1/TilemapLoaderPointers.cs @@ -0,0 +1,49 @@ +// +// 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; + +public class TilemapLoaderPointers +{ + public static readonly TilemapLoaderPointers Jp10 = new( + baseAddress: 0x058057); + + public static readonly TilemapLoaderPointers Jp11 = new( + baseAddress: 0x058057); + + public static readonly TilemapLoaderPointers Usa = new( + baseAddress: 0x058057); + + public static readonly TilemapLoaderPointers UsaPlusW = new( + baseAddress: 0x058057); + + public static readonly TilemapLoaderPointers Eu = new( + baseAddress: 0x058057); + + public static readonly TilemapLoaderPointers EuPlusW = new( + baseAddress: 0x058057); + + public static readonly TilemapLoaderPointers UsaSmb1 = new( + baseAddress: 0x028057); + + public TilemapLoaderPointers( + int tilemapDataIndexPointer, + int tilemapDataPointer) + { + TilemapDataIndexPointer = tilemapDataIndexPointer; + TilemapDataPointer = tilemapDataPointer; + } + + private TilemapLoaderPointers(int baseAddress) + : this( + tilemapDataIndexPointer: baseAddress, + tilemapDataPointer: baseAddress + 0x09) + { } + + public int TilemapDataIndexPointer { get; } + + public int TilemapDataPointer { get; } +} diff --git a/src/Snes/AddressMode.cs b/src/Snes/AddressMode.cs new file mode 100644 index 0000000..7d4ac34 --- /dev/null +++ b/src/Snes/AddressMode.cs @@ -0,0 +1,11 @@ +namespace Maseya.Snes; + +public enum AddressMode +{ + LoRom, + HiRom, + ExHiRom, + ExLoRom, + LoRom2, + HiRom2, +} diff --git a/src/Snes/CartridgeType.cs b/src/Snes/CartridgeType.cs new file mode 100644 index 0000000..bde31dc --- /dev/null +++ b/src/Snes/CartridgeType.cs @@ -0,0 +1,32 @@ +namespace Maseya.Snes; + +public enum CartridgeType +{ + RomOnly = 0x00, + RomRam = 0x01, + RamBattery = 0x02, + RomDsp = 0x03, + RomDspRam = 0x04, + RomDspRamBattery = 0x05, + RomDspBattery = 0x06, + RomSuperFx = 0x13, + RomSuperFxRam = 0x14, + RomSuperFxRamBattery = 0x15, + RomSuperFxBattery = 0x16, + RoObc1 = 0x23, + RoObc1Ram = 0x24, + RoObc1RamBattery = 0x25, + RoObc1Battery = 0x26, + RomSa1 = 0x33, + RomSa1Ram = 0x34, + RomSa1RamBattery = 0x35, + RomSa1Battery = 0x36, + RomOther = 0xE3, + RomOtherRam = 0xE4, + RomOtherRamBattery = 0xE5, + RomOtherBattery = 0xE6, + RomCustomChip = 0xF3, + RomCustomChipRam = 0xF4, + RomCustomChipRamBattery = 0xF5, + RomCustomChipBattery = 0xF6, +} diff --git a/src/Snes/ChrTile.cs b/src/Snes/ChrTile.cs new file mode 100644 index 0000000..67c3c63 --- /dev/null +++ b/src/Snes/ChrTile.cs @@ -0,0 +1,209 @@ +// +// 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.Snes; + +using System; + +public struct ChrTile : IEquatable +{ + public const int SizeOf = sizeof(ushort); + + private const int TileIndexMask = 0x1FF; + private const int PaletteOffset = 9; + private const int PaletteMask = 7; + private const int PriorityOffset = 12; + private const int PriorityMask = 3; + private const int FlipOffset = 14; + private const int FlipXOffset = FlipOffset; + private const int FlipYOffset = FlipOffset + 1; + private const int FlipMask = 3; + + private ushort value; + + public ChrTile( + int tileIndex, + int paletteIndex, + LayerPriority layerPriority, + TileFlip tileFlip) + { + value = (ushort)(tileIndex & TileIndexMask); + Value |= (paletteIndex & PaletteMask) << PaletteOffset; + Value |= ((int)layerPriority & PriorityMask) << PriorityOffset; + Value |= ((int)tileFlip & FlipMask) << FlipOffset; + } + + private ChrTile(int value) + { + this.value = (ushort)value; + } + + public int Value + { + get + { + return value; + } + + set + { + this.value = (ushort)value; + } + } + + public int TileIndex + { + get + { + return Value & TileIndexMask; + } + + set + { + Value &= ~TileIndexMask; + Value |= value & TileIndexMask; + } + } + + public int PaletteIndex + { + get + { + return (Value >> PaletteOffset) & PaletteMask; + } + + set + { + Value &= ~(PaletteMask << PaletteOffset); + Value |= (value & PaletteMask) << PaletteOffset; + } + } + + public LayerPriority Priority + { + get + { + return (LayerPriority)( + (Value >> PriorityOffset) & PriorityMask); + } + + set + { + Value &= ~(PriorityMask << PriorityOffset); + Value |= ((int)value & PriorityMask) << PriorityOffset; + } + } + + public TileFlip TileFlip + { + get + { + return (TileFlip)((Value >> FlipOffset) & FlipMask); + } + + set + { + Value &= ~(FlipMask << FlipOffset); + Value |= ((int)value & FlipMask) << FlipOffset; + } + } + + public bool XFlipped + { + get + { + return ((Value >> FlipXOffset) & 1) != 0; + } + + set + { + if (value) + { + Value |= 1 << FlipXOffset; + } + else + { + Value &= ~(1 << FlipXOffset); + } + } + } + + public int XFlipMask + { + get + { + return (-((value >> FlipXOffset) & 1)) & 7; + } + } + + public bool YFlipped + { + get + { + return ((Value >> FlipYOffset) & 1) != 0; + } + + set + { + if (value) + { + Value |= 1 << FlipYOffset; + } + else + { + Value &= ~(1 << FlipYOffset); + } + } + } + + public int YFlipMask + { + get + { + return (-((value >> FlipYOffset) & 1)) & 7; + } + } + + public static implicit operator int(ChrTile tile) + { + return tile.Value; + } + + public static implicit operator ChrTile(int value) + { + return new ChrTile(value); + } + + public static bool operator ==(ChrTile left, ChrTile right) + { + return left.Equals(right); + } + + public static bool operator !=(ChrTile left, ChrTile right) + { + return !(left == right); + } + + public bool Equals(ChrTile obj) + { + return Value.Equals(obj.Value); + } + + public override bool Equals(object? obj) + { + return obj is ChrTile tile && Equals(tile); + } + + public override int GetHashCode() + { + return Value; + } + + public override string ToString() + { + return $"{Value:X4}"; + } +} diff --git a/src/Snes/Color32BppArgb.cs b/src/Snes/Color32BppArgb.cs new file mode 100644 index 0000000..1fe0f10 --- /dev/null +++ b/src/Snes/Color32BppArgb.cs @@ -0,0 +1,124 @@ +// +// 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.Snes; + +using System; +using System.Drawing; +using System.Runtime.InteropServices; + +[StructLayout(LayoutKind.Sequential)] +public unsafe struct Color32BppArgb : IEquatable +{ + public byte B + { + get; + set; + } + + public byte G + { + get; + set; + } + + public byte R + { + get; + set; + } + + public byte A + { + get; + set; + } + + public int Value + { + get + { + return this; + } + } + + public Color32BppArgb(int alpha, int red, int green, int blue) + { + A = (byte)alpha; + R = (byte)red; + G = (byte)green; + B = (byte)blue; + } + + public static Color32BppArgb FromSnesColor(int low, int high) + { + return FromSnesColor(low | (high << 8)); + } + + public static Color32BppArgb FromSnesColor(int value) + { + return new Color32BppArgb( + Byte.MaxValue, + ((value >> (5 * 0)) & 0x1F) << 3, + ((value >> (5 * 1)) & 0x1F) << 3, + ((value >> (5 * 2)) & 0x1F) << 3); + } + + public static int ToSnesColor(Color32BppArgb color) + { + return (color.R >> 3) | ((color.G >> 3) << 5) | ((color.B >> 3) << 10); + } + + public static bool operator ==(Color32BppArgb left, Color32BppArgb right) + { + return left.Equals(right); + } + + public static bool operator !=(Color32BppArgb left, Color32BppArgb right) + { + return !(left == right); + } + + public static implicit operator int(Color32BppArgb pixel) + { + return *(int*)&pixel; + } + + public static implicit operator Color32BppArgb(int value) + { + return *(Color32BppArgb*)&value; + } + + public static implicit operator Color(Color32BppArgb pixel) + { + return Color.FromArgb(pixel); + } + + public static explicit operator Color32BppArgb(Color color) + { + return color.ToArgb(); + } + + public override int GetHashCode() + { + return Value; + } + + public bool Equals(Color32BppArgb other) + { + return Value == other.Value; + } + + public override bool Equals(object? obj) + { + return obj is Color32BppArgb other && Equals(other); + } + + public override string ToString() + { + return Value.ToString("X6"); + } +} diff --git a/src/Snes/DestinationCode.cs b/src/Snes/DestinationCode.cs new file mode 100644 index 0000000..2f0780b --- /dev/null +++ b/src/Snes/DestinationCode.cs @@ -0,0 +1,23 @@ +namespace Maseya.Snes; + +public enum DestinationCode +{ + Japan = 0x00, + NorthAmerica = 0x01, + Europe = 0x02, + Scandinavia = 0x03, + France = 0x06, + Dutch = 0x07, + Spanish = 0x08, + German = 0x09, + Italian = 0x0A, + Chinese = 0x0B, + Korean = 0x0D, + Common = 0x0E, + Canada = 0x0F, + Brazil = 0x10, + Australia = 0x11, + OtherVariationX = 0x12, + OtherVariationY = 0x13, + OtherVariationZ = 0x14, +} diff --git a/src/Snes/ExtensionMethods.cs b/src/Snes/ExtensionMethods.cs new file mode 100644 index 0000000..3c38a41 --- /dev/null +++ b/src/Snes/ExtensionMethods.cs @@ -0,0 +1,75 @@ +namespace Maseya.Snes; + +using System.ComponentModel; + +public static class ExtensionMethods +{ + public static bool IsLoRom(this AddressMode addressMode) + { + return addressMode switch + { + AddressMode.LoRom or + AddressMode.LoRom2 or + AddressMode.ExLoRom => true, + _ => false, + }; + } + + public static bool IsHiRom(this AddressMode addressMode) + { + return addressMode switch + { + AddressMode.HiRom or + AddressMode.HiRom2 or + AddressMode.ExHiRom => true, + _ => false, + }; + } + + public static bool IsExRom(this AddressMode addressMode) + { + return addressMode switch + { + AddressMode.ExLoRom or + AddressMode.ExHiRom => true, + _ => false, + }; + } + + public static int MinSize(this AddressMode addressMode) + { + return addressMode switch + { + AddressMode.LoRom => 0x8000, + AddressMode.HiRom => 0x1_0000, + AddressMode.ExHiRom => 0x41_0000, + AddressMode.ExLoRom => 0x40_8000, + AddressMode.LoRom2 => 0x8000, + AddressMode.HiRom2 => 0x1_0000, + _ => throw new InvalidEnumArgumentException( + nameof(addressMode), + (int)addressMode, + typeof(AddressMode)), + }; + } + + public static bool IsFastRom(this AddressMode addressMode) + { + return addressMode switch + { + AddressMode.LoRom2 or + AddressMode.HiRom2 => true, + _ => false, + }; + } + + public static bool IsFastMode(this MapMode mapMode) + { + return (mapMode & MapMode.Mode20Fast) == MapMode.Mode20Fast; + } + + public static bool IsHiRom(this MapMode mapMode) + { + return ((int)mapMode & 1) != 0; + } +} diff --git a/src/Snes/GfxConverter.cs b/src/Snes/GfxConverter.cs new file mode 100644 index 0000000..6820dd0 --- /dev/null +++ b/src/Snes/GfxConverter.cs @@ -0,0 +1,188 @@ +// +// 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.Snes; + +using System; + +public static class GfxConverter +{ + public static int PixelMapSize(int gfxSize) + { + var tileCount = gfxSize / 0x20; + return 0x40 * tileCount; + } + + public static int PixelMapSize2Bpp(int gfxSize) + { + var tileCount = gfxSize / 0x10; + return 0x40 * tileCount; + } + + public static byte[] GfxToPixelMap(Span gfxSrc) + { + var tileCount = gfxSrc.Length / 0x20; + var result = new byte[0x40 * tileCount]; + GfxToPixelMap(gfxSrc, result); + return result; + } + + public static void GfxToPixelMap(Span gfxSrc, Span pixelDest) + { + var tileCount = gfxSrc.Length / 0x20; + if (pixelDest.Length < 0x40 * tileCount) + { + throw new ArgumentException( + "Destination array is an insufficient size", + nameof(pixelDest)); + } + + var pixelIndex = 0; + for (var tileIndex = 0; tileIndex < tileCount; tileIndex++) + { + for (var y = 0; y < 8; y++) + { + var offset = (tileIndex << 5) + (y << 1); + + var val1 = gfxSrc[offset + 0]; + var val2 = gfxSrc[offset + 1]; + var val3 = gfxSrc[offset + 0 + (2 * 8)]; + var val4 = gfxSrc[offset + 1 + (2 * 8)]; + + for (var x = 8; --x >= 0;) + { + pixelDest[pixelIndex++] = (byte)( + (((val1 >> x) & 1) << 0) | + (((val2 >> x) & 1) << 1) | + (((val3 >> x) & 1) << 2) | + (((val4 >> x) & 1) << 3)); + } + } + } + } + + public static byte[] PixelMapToGfx(Span pixelSrc) + { + var tileCount = pixelSrc.Length / 0x40; + var result = new byte[0x20 * tileCount]; + PixelMapToGfx(pixelSrc, result); + return result; + } + + public static void PixelMapToGfx(Span pixelSrc, Span gfxDest) + { + var tileCount = pixelSrc.Length / 0x40; + if (gfxDest.Length < 0x20 * tileCount) + { + throw new ArgumentException( + "Destination array is an insufficient size", + nameof(gfxDest)); + } + + for (var i = 0; i < tileCount; i++) + { + var pixelIndex = 0; + for (var y = 0; y < 8; y++) + { + byte val1 = 0; + byte val2 = 0; + byte val3 = 0; + byte val4 = 0; + for (var x = 8; --x >= 0;) + { + var value = pixelSrc[(i * 0x40) + pixelIndex++]; + val1 |= (byte)(((value >> 0) & 1) << x); + val2 |= (byte)(((value >> 1) & 1) << x); + val3 |= (byte)(((value >> 2) & 1) << x); + val4 |= (byte)(((value >> 3) & 1) << x); + } + + var offset = (i << 5) + (y << 1); + gfxDest[offset + 0] = val1; + gfxDest[offset + 1] = val2; + gfxDest[offset + 0 + (2 * 8)] = val3; + gfxDest[offset + 1 + (2 * 8)] = val4; + } + } + } + + public static byte[] Gfx2BppToPixelMap(Span gfxSrc) + { + var tileCount = gfxSrc.Length / 0x10; + var result = new byte[0x40 * tileCount]; + Gfx2BppToPixelMap(gfxSrc, result); + return result; + } + + public static void Gfx2BppToPixelMap(Span gfxSrc, Span pixelDest) + { + var tileCount = gfxSrc.Length / 0x10; + if (pixelDest.Length < 0x40 * tileCount) + { + throw new ArgumentException( + "Destination array is an insufficient size", + nameof(pixelDest)); + } + + var pixelIndex = 0; + for (var tileIndex = 0; tileIndex < tileCount; tileIndex++) + { + for (var y = 0; y < 8; y++) + { + var offset = (tileIndex << 4) + (y << 1); + + var val1 = gfxSrc[offset + 0]; + var val2 = gfxSrc[offset + 1]; + + for (var x = 8; --x >= 0;) + { + pixelDest[pixelIndex++] = (byte)( + (((val1 >> x) & 1) << 0) | + (((val2 >> x) & 1) << 1)); + } + } + } + } + + public static byte[] PixelMapToGfx2Bpp(Span src) + { + var tileCount = src.Length / 0x40; + var result = new byte[tileCount * 0x10]; + PixelMapToGfx2Bpp(src, result); + return result; + } + + public static void PixelMapToGfx2Bpp(Span pixelSrc, Span gfxDest) + { + var tileCount = pixelSrc.Length / 0x40; + if (gfxDest.Length < 0x10 * tileCount) + { + throw new ArgumentException( + "Destination array is an insufficient size", + nameof(gfxDest)); + } + + for (var i = 0; i < tileCount; i++) + { + var pixelIndex = 0; + for (var y = 0; y < 8; y++) + { + byte val1 = 0; + byte val2 = 0; + for (var x = 8; --x >= 0;) + { + var value = pixelSrc[(i * 8 * 8) + pixelIndex++]; + val1 |= (byte)(((value >> 0) & 1) << x); + val2 |= (byte)(((value >> 1) & 1) << x); + } + + var offset = (i << 4) + (y << 1); + gfxDest[offset + 0] = val1; + gfxDest[offset + 1] = val2; + } + } + } +} diff --git a/src/Snes/LayerPriority.cs b/src/Snes/LayerPriority.cs new file mode 100644 index 0000000..d7da03d --- /dev/null +++ b/src/Snes/LayerPriority.cs @@ -0,0 +1,15 @@ +// +// 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.Snes; + +public enum LayerPriority +{ + Priority0 = 0, + Priority1 = 1, + Priority2 = 2, + Priority3 = 3, +} diff --git a/src/Snes/MapMode.cs b/src/Snes/MapMode.cs new file mode 100644 index 0000000..7d71962 --- /dev/null +++ b/src/Snes/MapMode.cs @@ -0,0 +1,13 @@ +namespace Maseya.Snes; + +public enum MapMode +{ + Mode20 = 0x20, + Mode21 = 0x21, + Reserved = 0x22, + Mode23Sa1 = 0x23, + Mode25 = 0x25, + Mode20Fast = 0x30, + Mode21Fast = 0x31, + Mode25Fast = 0x35, +} diff --git a/src/Snes/MathHelper.cs b/src/Snes/MathHelper.cs new file mode 100644 index 0000000..34bb57d --- /dev/null +++ b/src/Snes/MathHelper.cs @@ -0,0 +1,29 @@ +namespace Maseya.Snes; + +public static class MathHelper +{ + /// + /// Calculates the smallest power of 2 that is an upper-bound for a given integer. + /// + /// + /// The integer to upper-bound. + /// + /// + /// An integer that is the smallest power of 2 that upper-bounds . + /// + public static int BitCeil(int x) + { + // check for the set bits + x |= x >> 1; + x |= x >> 2; + x |= x >> 4; + x |= x >> 8; + x |= x >> 16; + + // Then we remove all but the top bit by xor'ing the string of 1's with that + // string of 1's shifted one to the left, and we end up with just the one top + // bit followed by 0's. + return x ^ (x >> 1); + } +} diff --git a/src/Snes/Obj16Tile.cs b/src/Snes/Obj16Tile.cs new file mode 100644 index 0000000..957276d --- /dev/null +++ b/src/Snes/Obj16Tile.cs @@ -0,0 +1,161 @@ +// +// 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.Snes; + +using System; + +public struct Obj16Tile : IEquatable +{ + public const int NumberOfTiles = 4; + + public const int TopLeftIndex = 0; + public const int BottomLeftIndex = 1; + public const int TopRightIndex = 2; + public const int BottomRightIndex = 3; + + public const int SizeOf = NumberOfTiles * ObjTile.SizeOf; + + public Obj16Tile( + ObjTile topLeft, + ObjTile bottomLeft, + ObjTile topRight, + ObjTile bottomRight) + { + TopLeft = topLeft; + BottomLeft = bottomLeft; + TopRight = topRight; + BottomRight = bottomRight; + } + + public ObjTile TopLeft + { + get; + set; + } + + public ObjTile BottomLeft + { + get; + set; + } + + public ObjTile TopRight + { + get; + set; + } + + public ObjTile BottomRight + { + get; + set; + } + + public ObjTile this[int index] + { + get + { + return index switch + { + TopLeftIndex => TopLeft, + BottomLeftIndex => TopRight, + TopRightIndex => BottomLeft, + BottomRightIndex => BottomRight, + _ => throw new ArgumentOutOfRangeException( + nameof(index)), + }; + } + + set + { + switch (index) + { + case TopLeftIndex: + TopLeft = value; + return; + + case BottomLeftIndex: + TopRight = value; + return; + + case TopRightIndex: + BottomLeft = value; + return; + + case BottomRightIndex: + BottomRight = value; + return; + + default: + throw new ArgumentOutOfRangeException( + nameof(index)); + } + } + } + + public static bool operator ==(Obj16Tile left, Obj16Tile right) + { + return left.Equals(right); + } + + public static bool operator !=(Obj16Tile left, Obj16Tile right) + { + return !(left == right); + } + + public static int GetXCoordinate(int index) + { + return index / 2; + } + + public static int GetYCoordinate(int index) + { + return index % 2; + } + + public Obj16Tile FlipX() + { + return new Obj16Tile( + BottomLeft.FlipX(), + TopLeft.FlipX(), + BottomRight.FlipX(), + TopRight.FlipX()); + } + + public Obj16Tile FlipY() + { + return new Obj16Tile( + TopRight.FlipY(), + BottomRight.FlipY(), + TopLeft.FlipY(), + BottomLeft.FlipY()); + } + + public bool Equals(Obj16Tile other) + { + return + TopLeft.Equals(other.TopLeft) && + TopRight.Equals(other.TopRight) && + BottomLeft.Equals(other.BottomLeft) && + BottomRight.Equals(other.BottomRight); + } + + public override bool Equals(object? obj) + { + return obj is Obj16Tile tile && Equals(tile); + } + + public override int GetHashCode() + { + return HashCode.Combine(TopLeft, BottomLeft, TopRight, BottomRight); + } + + public override string ToString() + { + return $"{TopLeft}-{BottomLeft}-{TopRight}-{BottomRight}"; + } +} diff --git a/src/Snes/ObjTile.cs b/src/Snes/ObjTile.cs new file mode 100644 index 0000000..5cbf01c --- /dev/null +++ b/src/Snes/ObjTile.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.Snes; + +using System; + +public struct ObjTile : IEquatable +{ + public const int SizeOf = sizeof(ushort); + + private const int TileIndexMask = 0x3FF; + private const int PaletteOffset = 10; + private const int PaletteMask = 7; + private const int PriorityOffset = 13; + private const int PriorityMask = 1; + private const int FlipOffset = 14; + private const int FlipMask = 3; + + private ushort value; + + private ObjTile(int value) + { + this.value = (ushort)value; + } + + public int Value + { + get + { + return value; + } + + set + { + this.value = (ushort)value; + } + } + + public int TileIndex + { + get + { + return Value & TileIndexMask; + } + + set + { + Value &= ~TileIndexMask; + Value |= value & TileIndexMask; + } + } + + public int PaletteIndex + { + get + { + return (Value >> PaletteOffset) & PaletteMask; + } + + set + { + Value &= ~(PaletteMask << PaletteOffset); + Value |= (value & PaletteMask) << PaletteOffset; + } + } + + public LayerPriority Priority + { + get + { + return (LayerPriority)( + (Value >> PriorityOffset) & PriorityMask); + } + + set + { + if (value != LayerPriority.Priority0) + { + Value |= PriorityMask << PriorityOffset; + } + else + { + Value &= ~PriorityMask << PriorityOffset; + } + } + } + + public TileFlip TileFlip + { + get + { + return (TileFlip)((Value >> FlipOffset) & FlipMask); + } + + set + { + Value &= ~(FlipMask << FlipOffset); + Value |= ((int)value & FlipMask) << FlipOffset; + } + } + + public bool XFlipped + { + get + { + return + (TileFlip & TileFlip.Horizontal) != 0; + } + + set + { + if (value) + { + TileFlip &= ~TileFlip.Horizontal; + } + else + { + TileFlip |= TileFlip.Horizontal; + } + } + } + + public bool YFlipped + { + get + { + return (TileFlip & TileFlip.Veritcal) != 0; + } + + set + { + if (value) + { + TileFlip &= ~TileFlip.Veritcal; + } + else + { + TileFlip |= TileFlip.Veritcal; + } + } + } + + public static implicit operator int(ObjTile tile) + { + return tile.Value; + } + + public static implicit operator ObjTile(int value) + { + return new ObjTile(value); + } + + public static bool operator ==(ObjTile left, ObjTile right) + { + return left.Equals(right); + } + + public static bool operator !=(ObjTile left, ObjTile right) + { + return left.Value != right.Value; + } + + public ObjTile FlipX() + { + var tile = this; + tile.XFlipped ^= true; + return tile; + } + + public ObjTile FlipY() + { + var tile = this; + tile.YFlipped ^= true; + return tile; + } + + public bool Equals(ObjTile other) + { + return Value.Equals(other.Value); + } + + public override bool Equals(object? obj) + { + return obj is ObjTile tile && Equals(tile); + } + + public override int GetHashCode() + { + return Value; + } + + public override string ToString() + { + return Value.ToString("X4"); + } +} diff --git a/src/Snes/RamSize.cs b/src/Snes/RamSize.cs new file mode 100644 index 0000000..b980091 --- /dev/null +++ b/src/Snes/RamSize.cs @@ -0,0 +1,11 @@ +namespace Maseya.Snes; + +public enum RamSize +{ + NoRam = 0, + Size16Kbit = 0x01, + Size64Kbit = 0x03, + Size256Kbit = 0x05, + Size512Kbit = 0x06, + Size1Mbit = 0x07, +} diff --git a/src/Snes/Rom.cs b/src/Snes/Rom.cs new file mode 100644 index 0000000..adc37da --- /dev/null +++ b/src/Snes/Rom.cs @@ -0,0 +1,1478 @@ +// +// 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.Snes; + +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Text; + +using static MathHelper; + +public class Rom +{ + public const int DefaultHeaderSize = 0x200; + + public const int PageSize = 0x2000; + public const int PageMask = PageSize - 1; + + // TODO(nrg) + public const int LoRomBankSize = 0x8000; + public const int LoRomWordMask = LoRomBankSize - 1; + public const int BankMask = 0xFF_0000; + + public const int LoRomBankLimit = 0x80; + public const int LoRomSizeLimit = LoRomBankLimit * LoRomBankSize; + + public const int ExLoRomBankLimit = 0xFE; + public const int ExLoRomSizeLimit = ExLoRomBankLimit * LoRomBankSize; + + public const int MakerCodeAddress = 0xFFB0; + public const int MakerCodeSize = 2; + public const int GameCodeAddress = 0xFFB2; + public const int GameCodeSize = 4; + public const int ExpansionRAMSizeAddress = 0xFFBD; + public const int SpecialVersionAddress = 0xFFBE; + public const int CartTypeSubNumberAddress = 0xFFBF; + public const int GameTitleAddress = 0xFFC0; + public const int GameTitleSize = 0x16; + public const int MapModeAddress = 0xFFD5; + public const int CartridgeTypeAddress = 0xFFD6; + public const int RomSizeAddress = 0xFFD7; + public const int MinRomSize = 7; + public const int MaxRomSize = 0x0D; + public const int RomSizeBaseValue = 0x400; + public const int RamSizeAddress = 0xFFD8; + public const int DestinationCodeAddress = 0xFFD9; + public const int FixedValueAddress = 0xFFDA; + public const int IntendedFixedValue = 0x33; + public const int MaskRomVersionNumberAddress = 0xFFDB; + public const int ComplementCheckAddress = 0xFFDC; + public const int CheckSumAddress = 0xFFDE; + + public const int NativeCOPVectorAddress = 0xFFE4; + public const int NativeBRKVectorAddress = 0xFFE6; + public const int NativeAbortVectorAddress = 0xFFE8; + public const int NativeNMIVectorAddress = 0xFFEA; + public const int NativeResetVectorAddress = 0xFFEC; + public const int NativeIRQVectorAddress = 0xFFEE; + + public const int EmuCOPVectorAddress = 0xFFF4; + public const int EmuBRKVectorAddress = 0xFFF6; + public const int EmuAbortVectorAddress = 0xFFF8; + public const int EmuNMIVectorAddress = 0xFFFA; + public const int EmuResetVectorAddress = 0xFFFC; + public const int EmuIRQVectorAddress = 0xFFFE; + + private static readonly int JisX201CodePage = 50221; + + private static Encoding? _gameTitleEncoding = null; + + public Rom(byte[] data) + { + var headerSize = data.Length & PageMask; + if (headerSize is not 0 and not DefaultHeaderSize) + { + throw new ArgumentException( + "Rom does not have valid header size (0 or 512 bytes)."); + } + + AddressMode = GetAddressMode( + new ReadOnlySpan(data, headerSize, data.Length - headerSize)); + Data = new byte[data.Length]; + Array.Copy(data, Data, data.Length); + } + + private Rom(byte[] data, AddressMode addressMode) + { + Data = data; + AddressMode = addressMode; + } + + public int Size + { + get + { + return Data.Length; + } + } + + public bool HasHeader + { + get + { + return HeaderSize == DefaultHeaderSize; + } + } + + public int HeaderSize + { + get + { + return Data.Length & LoRomWordMask; + } + } + + public int HeaderlessSize + { + get + { + return Size - HeaderSize; + } + } + + public AddressMode AddressMode + { + get; + private set; + } + + /// + /// Gets or sets two alphanumeric ASCII bytes identifying your company. Ignored by + /// emulators; for ROM hackers and home-brewers, just insert whatever. + /// + public string MakerCode + { + get + { + var result = ReadBytes(MakerCodeAddress, MakerCodeSize); + return Encoding.ASCII.GetString(result); + } + + set + { + var bytes = Encoding.ASCII.GetBytes(value); + var realLength = Math.Min(bytes.Length - 1, MakerCodeSize); + var result = new byte[MakerCodeSize]; + Array.Copy(bytes, result, realLength); + WriteBytes(MakerCodeAddress, result); + } + } + + /// + /// Get or sets four alphanumeric ASCII bytes identifying your game. Ignored by + /// emulators; for ROM hackers and home-brewers, just insert whatever. + /// Exception: If the game code starts with Z and ends with J, it's a BS-X flash cartridge. + /// + public string GameCode + { + get + { + var result = ReadBytes(GameCodeAddress, GameCodeSize); + return Encoding.ASCII.GetString(result); + } + + set + { + var bytes = Encoding.ASCII.GetBytes(value); + var realLength = Math.Min(bytes.Length - 1, GameCodeSize); + var result = new byte[GameCodeSize]; + Array.Copy(bytes, result, realLength); + WriteBytes(GameCodeAddress, result); + } + } + + /// + /// Gets or sets the size of the expansion RAM installed in the game pak. + /// + public RamSize ExpansionRAMSize + { + get + { + return (RamSize)ReadByte(ExpansionRAMSizeAddress); + } + + set + { + WriteByte(ExpansionRAMSizeAddress, (int)value); + } + } + + public bool IsValidExpansionRamSize + { + get + { + return Enum.IsDefined(typeof(RamSize), ExpansionRAMSize); + } + } + + /// + /// Gets or sets a value that should be 0x00 under normal circumstances. This value + /// is usually used during promotional events. + /// + public int SpecialVersion + { + get + { + return ReadByte(SpecialVersionAddress); + } + + set + { + WriteByte(SpecialVersionAddress, value); + } + } + + /// + /// Gets or sets a value only assigned when it is necessary to distinguish between + /// games which use the same cartridge type. Should normally be 00H. + /// + public int CartTypeSubNumber + { + get + { + return ReadByte(CartTypeSubNumberAddress); + } + + set + { + WriteByte(CartTypeSubNumberAddress, value); + } + } + + /// + /// Gets or sets the game title, which is 21 bytes long, encoded with the JIS X + /// 0201 character set (which consists of standard ASCII plus katakana). If the + /// title is shorter than 21 bytes, then the remainder should be padded with spaces + /// (0x20). + /// + public string GameTitle + { + get + { + var result = ReadBytes(GameTitleAddress, GameTitleSize); + return GameTitleEncoding.GetString(result).TrimEnd().Replace('\\', '¥'); + } + + set + { + var bytes = GameTitleEncoding.GetBytes(value.Replace('¥', '\\')); + var realLength = Math.Min(bytes.Length - 1, GameTitleSize); + var result = new byte[GameTitleSize]; + Array.Fill(result, (byte)' '); + Array.Copy(bytes, result, realLength); + WriteBytes(GameTitleAddress, result); + } + } + + /// + /// Gets or sets a value determining whether this is a HiROM or LoROM game. + /// + public MapMode MapMode + { + get + { + return (MapMode)ReadByte(MapModeAddress); + } + + set + { + WriteByte(MapModeAddress, (int)value); + } + } + + public bool IsFastROM + { + get + { + return ((int)MapMode & 0x30) == 0x30; + } + } + + public bool IsHiROM + { + get + { + return ((int)MapMode & 1) != 0; + } + } + + public CartridgeType CartridgeType + { + get + { + return (CartridgeType)ReadByte(CartridgeTypeAddress); + } + + set + { + WriteByte(CartridgeTypeAddress, (int)value); + } + } + + public bool IsCartridgeTypeValid + { + get + { + return Enum.IsDefined(typeof(CartridgeType), CartridgeType); + } + } + + public RomSize RomSize + { + get + { + return (RomSize)ReadByte(RomSizeAddress); + } + + set + { + WriteByte(RomSizeAddress, (int)value); + } + } + + public int RomCapacity + { + get + { + return RomSizeBaseValue << (int)RomSize; + } + } + + public bool IsRomSizeValid + { + get + { + return Enum.IsDefined(typeof(RomSize), RomSize) + && HeaderlessSize <= RomCapacity + && HeaderlessSize > RomCapacity >> 1; + } + } + + public RamSize RamSize + { + get + { + return (RamSize)ReadByte(RamSizeAddress); + } + + set + { + WriteByte(RamSizeAddress, (int)value); + } + } + + public bool IsRamSizeValid + { + get + { + return Enum.IsDefined(typeof(RamSize), RamSize); + } + } + + public DestinationCode DestinationCode + { + get + { + return (DestinationCode)ReadByte(DestinationCodeAddress); + } + + set + { + WriteByte(DestinationCodeAddress, (int)value); + } + } + + public bool IsDestinationCodeValid + { + get + { + return Enum.IsDefined(typeof(DestinationCode), DestinationCode); + } + } + + /// + /// Gets or sets a value that should always be 0x33. + /// + public int FixedValue + { + get + { + return ReadByte(FixedValueAddress); + } + + set + { + WriteByte(FixedValueAddress, value); + } + } + + public bool IsFixedValueValid + { + get + { + return FixedValue == IntendedFixedValue; + } + } + + /// + /// Gets or sets a value that stores the version number of the ROM released to the + /// market as a product. The number begins with 0 at production and increases with + /// each revised version. + /// + public int MaskRomVersion + { + get + { + return ReadByte(MaskRomVersionNumberAddress); + } + + set + { + WriteByte(MaskRomVersionNumberAddress, value); + } + } + + /// + /// Gets or sets the 16-bit complements (bit-inverse) of . + /// + /// + /// Summing and should always + /// equal 0xFFFF. + /// + public int ComplementCheck + { + get + { + return ReadInt16(ComplementCheckAddress); + } + + set + { + WriteInt16(ComplementCheckAddress, value); + } + } + + /// + /// Gets or sets the checksum of the ROM. + /// + public int Checksum + { + get + { + return ReadInt16(CheckSumAddress); + } + + set + { + WriteInt16(CheckSumAddress, value); + } + } + + public bool IsComplementCheckValid + { + get + { + return (Checksum ^ ComplementCheck) == Int16.MaxValue; + } + } + + public int RealChecksum + { + get + { + var sum = 0; + for (var i = HeaderSize; i < Data.Length; i++) + { + sum += Data[i]; + } + + var repeatBase = BitCeil(HeaderlessSize) / 2; + var remainder = HeaderlessSize - repeatBase; + for (var i = HeaderlessSize; i < RomCapacity; i++) + { + sum += Data[HeaderSize + repeatBase + (i % remainder)]; + } + + return sum & UInt16.MaxValue; + } + } + + public bool IsChecksumValid + { + get + { + return Checksum == RealChecksum; + } + } + + public int NativeCOPVector + { + get + { + return ReadInt16(NativeCOPVectorAddress); + } + + set + { + WriteInt16(NativeCOPVectorAddress, value); + } + } + + public int NativeBRKVector + { + get + { + return ReadInt16(NativeBRKVectorAddress); + } + + set + { + WriteInt16(NativeBRKVectorAddress, value); + } + } + + public int NativeAbortVector + { + get + { + return ReadInt16(NativeAbortVectorAddress); + } + + set + { + WriteInt16(NativeAbortVectorAddress, value); + } + } + + public int NativeNMIVector + { + get + { + return ReadInt16(NativeNMIVectorAddress); + } + + set + { + WriteInt16(NativeNMIVectorAddress, value); + } + } + + public int NativeResetVector + { + get + { + return ReadInt16(NativeResetVectorAddress); + } + + set + { + WriteInt16(NativeResetVectorAddress, value); + } + } + + public int NativeIRQVector + { + get + { + return ReadInt16(NativeIRQVectorAddress); + } + + set + { + WriteInt16(NativeIRQVectorAddress, value); + } + } + + public int EmuCOPVector + { + get + { + return ReadInt16(EmuCOPVectorAddress); + } + + set + { + WriteInt16(EmuCOPVectorAddress, value); + } + } + + public int EmuBRKVector + { + get + { + return ReadInt16(EmuBRKVectorAddress); + } + + set + { + WriteInt16(EmuBRKVectorAddress, value); + } + } + + public int EmuNMIVector + { + get + { + return ReadInt16(EmuNMIVectorAddress); + } + + set + { + WriteInt16(EmuNMIVectorAddress, value); + } + } + + public int EmuResetVector + { + get + { + return ReadInt16(EmuResetVectorAddress); + } + + set + { + WriteInt16(EmuResetVectorAddress, value); + } + } + + public int EmuIRQVector + { + get + { + return ReadInt16(EmuIRQVectorAddress); + } + + set + { + WriteInt16(EmuIRQVectorAddress, value); + } + } + + public static Encoding GameTitleEncoding + { + get + { + if (_gameTitleEncoding is null) + { + Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); + _gameTitleEncoding = Encoding.GetEncoding( + codepage: JisX201CodePage); + } + + return _gameTitleEncoding; + } + } + + private byte[] Data + { + get; + } + + private bool IsValidAddressMode + { + get + { + return HeaderlessSize >= AddressMode.MinSize() + && AddressMode.IsHiRom() == IsHiROM + && (AddressMode.IsExRom() || AddressMode.IsFastRom() == IsFastROM) + && !IsComplementCheckValid + && IsRomSizeValid; + } + } + + public static bool IsValidAddress(int pointer, AddressMode mode) + { + return pointer >= 0 + && mode switch + { + AddressMode.LoRom => IsValidLoRomPointer(pointer), + AddressMode.LoRom2 => IsValidLoRom2Pointer(pointer), + AddressMode.ExLoRom => IsValidExLoRomPointer(pointer), + AddressMode.HiRom => IsValidHiRomPointer(pointer), + AddressMode.HiRom2 => IsValidHiRom2Pointer(pointer), + AddressMode.ExHiRom => IsValidExHiRomPointer(pointer), + _ => throw new InvalidEnumArgumentException( + nameof(mode), + (int)mode, + typeof(AddressMode)), + }; + } + + public static int IncrementSnesAddress( + int snesAddress, + int amount, + bool crossBanks = true) + { + if (crossBanks) + { + return snesAddress + amount; + } + + var bank = snesAddress & BankMask; + var word = snesAddress & LoRomWordMask; + return bank | 0x8000 | ((word + amount) & LoRomWordMask); + } + + public static int SnesToPc( + int pointer, + bool hasHeader, + AddressMode mode) + { + if (pointer < 0) + { + throw new ArgumentOutOfRangeException( + nameof(pointer), + "SNES address cannot be less than zero."); + } + + var header = hasHeader ? DefaultHeaderSize : 0; + int result; + switch (mode) + { + case AddressMode.LoRom: + case AddressMode.LoRom2: + result = ((pointer & 0x7F_0000) >> 1) | (pointer & 0x7FFF); + break; + + case AddressMode.HiRom: + case AddressMode.HiRom2: + result = pointer & 0x3F_FFFF; + break; + + case AddressMode.ExHiRom: + result = pointer & 0x3F_FFFF; + if (pointer < 0xC0_0000) + { + result |= 0x40_0000; + } + + break; + + case AddressMode.ExLoRom: + result = ((pointer & 0x7F_0000) >> 1) | (pointer & 0x7FFF); + if (pointer < 0x80_0000) + { + result |= 0x40_0000; + } + + break; + + default: + throw new InvalidEnumArgumentException( + nameof(mode), + (int)mode, + typeof(AddressMode)); + } + + return result + header; + } + + public static int PcToSnes(int pointer, bool hasHeader, AddressMode mode) + { + pointer -= hasHeader ? DefaultHeaderSize : 0; + if (pointer < 0) + { + throw new ArgumentOutOfRangeException( + nameof(pointer), + "PC Address cannot be less than zero."); + } + + int result; + switch (mode) + { + case AddressMode.LoRom: + result = ((pointer << 1) & 0x7F_0000) | 0x8000 | (pointer & 0x7FFF); + if (pointer >= 0x38_0000) + { + result |= 0x80_0000; + } + + break; + + case AddressMode.HiRom: + result = 0xC0_0000 | pointer; + break; + + case AddressMode.ExHiRom: + result = pointer >= 0x7E_0000 + ? ~0x40_0000 & pointer + : pointer < 0x40_0000 + ? 0xC0_0000 | pointer + : pointer; + break; + + case AddressMode.ExLoRom: + result = ((pointer << 1) & 0x7F_0000) | 0x8000 | (pointer & 0x7FFF); + if (pointer < 0x40_0000) + { + result |= 0x80_0000; + } + + break; + + case AddressMode.LoRom2: + result = 0x80_0000 + | ((pointer << 1) & 0x7F_0000) | 0x8000 | (pointer & 0x7FFF); + break; + + case AddressMode.HiRom2: + result = 0x40_0000 | pointer; + if (pointer >= 0x30_0000) + { + result |= 0x80_0000; + } + + break; + + default: + throw new InvalidEnumArgumentException( + nameof(mode), + (int)mode, + typeof(AddressMode)); + } + + return result; + } + + public bool IsValidAddress(int pointer) + { + return IsValidAddress(pointer, AddressMode) + && SnesToPc(pointer) < RomCapacity; + } + + public byte ReadByte(int snesAddress) + { + var pcAddress = SnesToPc(snesAddress); + return Data[pcAddress]; + } + + public byte ReadByteIndirect(int snesAddress) + { + return ReadByteIndirectIndexed(snesAddress, 0, false); + } + + public byte ReadByteIndirectIndexed( + int snesAddress, + int index, + bool crossBanks = true) + { + var bank = snesAddress & BankMask; + var word = ReadInt16(snesAddress); + var address = IncrementSnesAddress(bank | word, index, crossBanks); + return ReadByte(address); + } + + public int ReadInt16(int snesAddress, bool crossBanks = true) + { + return ReadInt16( + snesAddress, + IncrementSnesAddress(snesAddress, 1, crossBanks)); + } + + public int ReadInt16(int snesAddressLow, int snesAddressHigh) + { + var low = ReadByte(snesAddressLow); + var high = ReadByte(snesAddressHigh); + return low | (high << 8); + } + + public int ReadInt16Indirect(int snesAddress) + { + return ReadInt16IndirectIndexed(snesAddress, 0); + } + + public int ReadInt16Indirect(int snesAddressLow, int snesAddressHigh) + { + return ReadByteIndirect(snesAddressLow) + | (ReadByteIndirect(snesAddressHigh) << 8); + } + + public int ReadInt16IndirectIndexed( + int snesAddress, + int index, + bool crossBanks = true) + { + var bank = snesAddress & BankMask; + var word = ReadInt16(snesAddress); + var address = IncrementSnesAddress(bank | word, index << 1, crossBanks); + return ReadInt16(address, crossBanks); + } + + public int ReadInt16IndirectIndexed( + int snesAddressLow, + int snesAddressHigh, + int index, + bool crossBanks = true) + { + return ReadByteIndirectIndexed(snesAddressLow, index, crossBanks) + | (ReadByteIndirectIndexed(snesAddressHigh, index, crossBanks) << 8); + } + + public void ReadBytes( + int snesAddress, + int count, + Span dest, + bool crossBanks = true) + { + ReadBytes(snesAddress, dest[..count], crossBanks); + } + + public void ReadBytes( + int snesAddress, + Span dest, + bool crossBanks = true) + { + var count = dest.Length; + var subCount = crossBanks + ? count + : LoRomBankSize - (snesAddress & LoRomWordMask); + var index = 0; + while (count > 0) + { + var pcAddress = SnesToPc(snesAddress); + new Span(Data, pcAddress, subCount).CopyTo(dest[index..]); + + index += subCount; + count -= subCount; + subCount = Math.Min(count, LoRomBankSize); + snesAddress &= BankMask; + } + } + + public byte[] ReadBytes( + int snesAddress, + int count, + bool crossBanks = true) + { + var result = new byte[count]; + ReadBytes(snesAddress, result, crossBanks); + return result; + } + + public IEnumerable EnumerateBytes( + int snesAddress, + int count, + bool crossBanks = true) + { + var pcAddress = SnesToPc(snesAddress); + if (crossBanks) + { + while (count > 0) + { + yield return Data[pcAddress]; + count--; + if (++pcAddress == Data.Length) + { + pcAddress = HeaderSize; + } + } + } + else + { + var subCount = LoRomBankSize - (snesAddress & LoRomWordMask); + while (count > 0) + { + for (var i = 0; i < subCount && count > 0; i++) + { + yield return Data[pcAddress + i]; + count--; + } + + subCount = BankMask; + pcAddress &= ~LoRomWordMask; + } + } + } + + public void ReadBytesIndirect( + int snesAddress, + int count, + Span dest, + bool crossBanks = true) + { + ReadBytesIndirect(snesAddress, dest[..count], crossBanks); + } + + public void ReadBytesIndirect( + int snesAddress, + Span dest, + bool crossBanks = true) + { + ReadBytesIndirectIndexed(snesAddress, 0, dest, crossBanks); + } + + public byte[] ReadBytesIndirect( + int snesAddress, + int count, + bool crossBanks = true) + { + var result = new byte[count]; + ReadBytesIndirect(snesAddress, result, crossBanks); + return result; + } + + public void ReadBytesIndirectIndexed( + int snesAddress, + int index, + int count, + Span dest, + bool crossBanks = true) + { + ReadBytesIndirectIndexed( + snesAddress, + index, + dest[..count], + crossBanks); + } + + public void ReadBytesIndirectIndexed( + int snesAddress, + int index, + Span dest, + bool crossBanks = true) + { + var bank = snesAddress & BankMask; + var word = ReadInt16(snesAddress); + var address = IncrementSnesAddress(bank | word, index, crossBanks); + ReadBytes(address, dest, crossBanks); + } + + public byte[] ReadBytesIndirectIndexed( + int snesAddress, + int index, + int count, + bool crossBanks = true) + { + var result = new byte[count]; + ReadBytesIndirectIndexed(snesAddress, index, result, crossBanks); + return result; + } + + public void ReadInt16Array( + int snesAddress, + int count, + Span dest, + bool crossBanks = true) + { + ReadInt16Array(snesAddress, dest[..count], crossBanks); + } + + public void ReadInt16Array( + int snesAddress, + Span dest, + bool crossBanks = true) + { + unsafe + { + fixed (short* ptr = dest) + { + ReadBytes( + snesAddress, + dest.Length * sizeof(short), + new Span((byte*)ptr, dest.Length * sizeof(short)), + crossBanks); + } + } + } + + public short[] ReadInt16Array( + int snesAddress, + int count, + bool crossBanks = true) + { + var result = new short[count]; + ReadInt16Array(snesAddress, result, crossBanks); + return result; + } + + public void ReadInt16ArrayIndirect( + int snesAddress, + int count, + Span dest, + bool crossBanks = true) + { + ReadInt16ArrayIndirect(snesAddress, dest[..count], crossBanks); + } + + public void ReadInt16ArrayIndirect( + int snesAddress, + Span dest, + bool crossBanks = true) + { + var bank = snesAddress & BankMask; + var word = ReadInt16(snesAddress); + ReadInt16Array(bank | word, dest, crossBanks); + } + + public short[] ReadInt16ArrayIndirect( + int snesAddress, + int count, + bool crossBanks = true) + { + var result = new short[count]; + ReadInt16ArrayIndirect(snesAddress, result, crossBanks); + return result; + } + + public void ReadInt16ArrayIndirectIndexed( + int snesAddress, + int index, + int count, + Span dest, + bool crossBanks = true) + { + ReadInt16ArrayIndirectIndexed( + snesAddress, + index, + dest[..count], + crossBanks); + } + + public void ReadInt16ArrayIndirectIndexed( + int snesAddress, + int index, + Span dest, + bool crossBanks = true) + { + var bank = snesAddress & BankMask; + var word = ReadInt16(snesAddress); + var address = IncrementSnesAddress(bank | word, index, crossBanks); + ReadInt16Array(address, dest, crossBanks); + } + + public short[] ReadInt16ArrayIndirectIndexed( + int snesAddress, + int index, + int count, + bool crossBanks = true) + { + var result = new short[count]; + ReadInt16ArrayIndirectIndexed(snesAddress, index, result, crossBanks); + return result; + } + + public void ReadInt16ArrayAs( + int snesAddress, + int count, + Span dest, + Func func, + bool crossBanks = true) + { + ReadInt16ArrayAs( + snesAddress, + dest[..count], + func, + crossBanks); + } + + public void ReadInt16ArrayAs( + int snesAddress, + Span dest, + Func func, + bool crossBanks = true) + { + var source = ReadInt16Array(snesAddress, dest.Length, crossBanks); + for (var i = 0; i < dest.Length; i++) + { + dest[i] = func(source[i]); + } + } + + public T[] ReadInt16ArrayAs( + int snesAddress, + int count, + Func func, + bool crossBanks = true) + { + var result = new T[count]; + ReadInt16ArrayAs(snesAddress, count, result, func, crossBanks); + return result; + } + + public void ReadInt16ArrayIndirectAs( + int snesAddress, + int count, + Span dest, + Func func, + bool crossBanks = true) + { + ReadInt16ArrayIndirectAs( + snesAddress, + dest[..count], + func, + crossBanks); + } + + public void ReadInt16ArrayIndirectAs( + int snesAddress, + Span dest, + Func func, + bool crossBanks = true) + { + var bank = snesAddress & BankMask; + var word = ReadInt16(snesAddress); + ReadInt16ArrayAs(bank | word, dest, func, crossBanks); + } + + public T[] ReadInt16ArrayIndirectAs( + int snesAddress, + int count, + Func func, + bool crossBanks = true) + { + var result = new T[count]; + ReadInt16ArrayIndirectAs(snesAddress, result, func, crossBanks); + return result; + } + + public void WriteByte(int snesAddress, int value) + { + var pcAddress = SnesToPc(snesAddress); + Data[pcAddress] = (byte)value; + } + + public void WriteByteIndirect(int snesAddress, int value) + { + var bank = snesAddress & BankMask; + var word = ReadInt16(snesAddress); + WriteByte(bank | word, value); + } + + public void WriteInt16( + int snesAddress, + int value, + bool crossBanks = true) + { + WriteInt16( + snesAddress, + IncrementSnesAddress(snesAddress, 1, crossBanks), + value); + } + + public void WriteInt16Indirect( + int snesAddress, + int value, + bool crossBanks = true) + { + var bank = snesAddress & BankMask; + var word = ReadInt16(snesAddress); + WriteInt16(bank | word, value, crossBanks); + } + + public void WriteInt16( + int snesAddressLow, + int snesAddressHigh, + int value) + { + WriteByte(snesAddressLow, value); + WriteByte(snesAddressHigh, value >> 8); + } + + public void WriteInt16Indirect( + int snesAddressLow, + int snesAddressHigh, + int value) + { + WriteByteIndirect(snesAddressLow, value); + WriteByteIndirect(snesAddressHigh, value >> 8); + } + + public void WriteBytes( + int snesAddress, + Span bytes, + bool crossBanks = true) + { + var pcAddress = SnesToPc(snesAddress); + if (crossBanks || IsArrayInBank(bytes.Length, snesAddress)) + { + bytes.CopyTo(new Span(Data, pcAddress, bytes.Length)); + } + else + { + throw new NotImplementedException(); + } + } + + public void WriteBytesIndirect( + int snesAddress, + Span bytes, + bool crossBanks = true) + { + WriteBytesIndirectIndexed(snesAddress, 0, bytes, crossBanks); + } + + public void WriteBytesIndirectIndexed( + int snesAddress, + int index, + Span bytes, + bool crossBanks = true) + { + var bank = snesAddress & BankMask; + var word = ReadInt16(snesAddress); + var address = IncrementSnesAddress(bank | word, index, crossBanks); + WriteBytes(address, bytes, crossBanks); + } + + public void WriteInt16Array( + int snesAddress, + Span words, + bool crossBanks = true) + { + unsafe + { + fixed (short* ptr = words) + { + WriteBytes( + snesAddress, + new Span((byte*)ptr, words.Length * sizeof(short)), + crossBanks); + } + } + } + + public void WriteInt16ArrayIndirect( + int snesAddress, + Span words, + bool crossBanks = true) + { + WriteInt16ArrayIndirectIndexed(snesAddress, 0, words, crossBanks); + } + + public void WriteInt16ArrayIndirectIndexed( + int snesAddress, + int index, + Span words, + bool crossBanks = true) + { + var bank = snesAddress & BankMask; + var word = ReadInt16(snesAddress); + var address = IncrementSnesAddress(bank | word, index, crossBanks); + WriteInt16Array(address, words, crossBanks); + } + + public void WriteArrayAsInt16( + int snesAddress, + Span data, + Func func, + bool crossBanks = true) + { + var source = new short[data.Length]; + for (var i = 0; i < data.Length; i++) + { + source[i] = func(data[i]); + } + + WriteInt16Array(snesAddress, source, crossBanks); + } + + public void WriteArrayAsInt16Indirect( + int snesAddress, + Span data, + Func func, + bool crossBanks = true) + { + var bank = snesAddress & BankMask; + var word = ReadInt16(snesAddress); + WriteArrayAsInt16(bank | word, data, func, crossBanks); + } + + public byte[] GetData() + { + var result = new byte[Data.Length]; + Array.Copy(Data, result, Data.Length); + return result; + } + + private static bool IsArrayInBank(int count, int snesAddress) + { + return count + (snesAddress & LoRomWordMask) <= LoRomBankSize; + } + + private static bool IsValidLoRomPointer(int pointer) + { + return (pointer & 0x8000) != 0 && (pointer & ~0xFFFF) < 0x70_0000; + } + + private static bool IsValidLoRom2Pointer(int pointer) + { + return (pointer & 0x8000) != 0 + && (pointer & 0x80_0000) != 0 + && (pointer & ~0x80FFFF) < 0x70_0000; + } + + private static bool IsValidExLoRomPointer(int pointer) + { + return (pointer & 0x8000) != 0 + && (pointer & 0xFF_0000) != 0x7E_0000 + && (pointer & 0xFF_0000) != 0x7F_0000 + && (pointer & ~0xFFFF) < 0x10_0000; + } + + private static bool IsValidHiRomPointer(int pointer) + { + return (pointer & 0xC0_0000) != 0 && (pointer & ~0xC0_0000) < 0x40_0000; + } + + private static bool IsValidHiRom2Pointer(int pointer) + { + return + ((pointer & 0xC0_0000) == 0x40_0000 && (pointer & ~0xC0_0000) < 0x30_0000) + || ((pointer & 0xC0_0000) == 0xC0_0000 && (pointer & ~0xC0_0000) < 0x40_0000); + } + + private static bool IsValidExHiRomPointer(int pointer) + { + return pointer < 0x3E_0000 + || (uint)(pointer - 0xC0_0000) < 0x10_00000 - 0xC0_0000 + || (uint)(pointer - 0x40_0000) < 0x7E_0000 - 0x40_0000; + } + + private static AddressMode GetAddressMode(ReadOnlySpan headerlessData) + { + var loRomHeader = new RomHeader( + headerlessData.Length, + headerlessData.Slice(0x7FB0, 0x50), + AddressMode.LoRom); + var bestHeader = loRomHeader; + + if (headerlessData.Length >= 0x1_0000) + { + var hiRomHeader = new RomHeader( + headerlessData.Length, + headerlessData.Slice(0xFFB0, 0x50), + AddressMode.HiRom); + if (bestHeader.Score < hiRomHeader.Score) + { + bestHeader = hiRomHeader; + } + + if (headerlessData.Length >= 0x40_8000) + { + var exLoRomHeader = new RomHeader( + headerlessData.Length, + headerlessData.Slice(0x407FB0, 0x50), + AddressMode.ExLoRom); + if (bestHeader.Score < exLoRomHeader.Score) + { + bestHeader = exLoRomHeader; + } + + if (headerlessData.Length >= 0x41_0000) + { + var exHiRomHeader = new RomHeader( + headerlessData.Length, + headerlessData.Slice(0x40FFB0, 0x50), + AddressMode.ExHiRom); + if (bestHeader.Score < exHiRomHeader.Score) + { + bestHeader = exHiRomHeader; + } + } + } + } + + return bestHeader.Score > 0 + ? bestHeader.AddressMode + : throw new ArgumentException("Could not find suitable address mode."); + } + + private int SnesToPc(int pointer) + { + return SnesToPc(pointer, HasHeader, AddressMode); + } +} diff --git a/src/Snes/RomHeader.cs b/src/Snes/RomHeader.cs new file mode 100644 index 0000000..4266405 --- /dev/null +++ b/src/Snes/RomHeader.cs @@ -0,0 +1,240 @@ +namespace Maseya.Snes; + +using System; +using System.Linq; +using System.Text; + +internal readonly ref struct RomHeader +{ + public RomHeader( + int fullRomSize, + ReadOnlySpan data, + AddressMode addressMode) + { + FullRomSize = fullRomSize; + Data = data; + AddressMode = addressMode; + } + + public string MakerCode + { + get + { + var result = Data[..Rom.MakerCodeSize]; + return Encoding.ASCII.GetString(result); + } + } + + public RamSize ExpansionRAMSize + { + get + { + return (RamSize)Data[Rom.ExpansionRAMSizeAddress - 0xFFB0]; + } + } + + public string GameTitle + { + get + { + var result = Data.Slice(Rom.GameTitleAddress - 0xFFB0, Rom.GameTitleSize); + return Rom.GameTitleEncoding.GetString(result).TrimEnd().Replace('\\', '¥'); + } + } + + /// + /// Gets or sets a value determining whether this is a HiROM or LoROM game. + /// + public MapMode MapMode + { + get + { + return (MapMode)Data[Rom.MapModeAddress - 0xFFB0]; + } + } + + public CartridgeType CartridgeType + { + get + { + return (CartridgeType)Data[Rom.CartridgeTypeAddress - 0xFFB0]; + } + } + + public RomSize RomSize + { + get + { + return (RomSize)Data[Rom.RomSizeAddress - 0xFFB0]; + } + } + + public int RomCapacity + { + get + { + return Rom.RomSizeBaseValue << (int)RomSize; + } + } + + public RamSize RamSize + { + get + { + return (RamSize)Data[Rom.RamSizeAddress - 0xFFB0]; + } + } + + public DestinationCode DestinationCode + { + get + { + return (DestinationCode)Data[Rom.DestinationCodeAddress - 0xFFB0]; + } + } + + public int FixedValue + { + get + { + return Data[Rom.FixedValueAddress - 0xFFB0]; + } + } + + public bool IsFixedValueValid + { + get + { + return FixedValue == Rom.IntendedFixedValue; + } + } + + public int ComplementCheck + { + get + { + return ReadInt16(Rom.ComplementCheckAddress - 0xFFB0); + } + } + + public int Checksum + { + get + { + return ReadInt16(Rom.CheckSumAddress - 0xFFB0); + } + } + + public bool IsComplementCheckValid + { + get + { + return (Checksum ^ ComplementCheck) == UInt16.MaxValue; + } + } + + public int EmuResetVector + { + get + { + return ReadInt16(Rom.EmuResetVectorAddress - 0xFFB0); + } + } + + public int Score + { + get + { + var score = 0; + if (RomSize == RomSize.Size64Mbit && FullRomSize > 0x40_0000) + { + score += 5; + if (AddressMode.IsExRom()) + { + score++; + } + } + + if (MapMode.IsHiRom()) + { + if (AddressMode.IsHiRom()) + { + score += 2; + } + } + else if (AddressMode.IsLoRom()) + { + score += 3; + } + + if (MapMode == MapMode.Mode23Sa1) + { + if (AddressMode.IsHiRom()) + { + score -= 2; + } + } + else if (AddressMode.IsLoRom()) + { + score += 2; + } + + if (IsComplementCheckValid) + { + score += 2; + if (Checksum != 0) + { + score++; + } + } + + if (IsFixedValueValid) + { + score += 2; + } + + if (((int)MapMode & 0x0F) < 4) + { + score += 2; + } + + if ((EmuResetVector & 0x8000) == 0) + { + score -= 6; + } + + if (EmuResetVector > 0xFFB0) + { + score -= 2; + } + + if (!Enum.IsDefined(RomSize)) + { + score--; + } + + if (!MakerCode.All(Char.IsAscii)) + { + score--; + } + + if (!GameTitle.All(Char.IsAscii)) + { + score--; + } + + return score; + } + } + + public AddressMode AddressMode { get; } + + public ReadOnlySpan Data { get; } + + public int FullRomSize { get; } + + private int ReadInt16(int address) + { + return Data[address] | (Data[address + 1] << 8); + } +} diff --git a/src/Snes/RomSize.cs b/src/Snes/RomSize.cs new file mode 100644 index 0000000..129877f --- /dev/null +++ b/src/Snes/RomSize.cs @@ -0,0 +1,10 @@ +namespace Maseya.Snes; + +public enum RomSize +{ + Size4Mbit = 0x09, + Size8Mbit = 0x0A, + Size16Mbit = 0x0B, + Size32Mbit = 0x0C, + Size64Mbit = 0x0D, +} diff --git a/src/Snes/Snes.csproj b/src/Snes/Snes.csproj new file mode 100644 index 0000000..c7f741b --- /dev/null +++ b/src/Snes/Snes.csproj @@ -0,0 +1,16 @@ + + + + net8.0 + enable + enable + True + Maseya.$(MSBuildProjectName.Replace(" ", "_")) + en-US + + + + + + + diff --git a/src/Snes/Sprite.cs b/src/Snes/Sprite.cs new file mode 100644 index 0000000..52eb56a --- /dev/null +++ b/src/Snes/Sprite.cs @@ -0,0 +1,77 @@ +// +// 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.Snes; + +using System; + +public struct Sprite : IEquatable +{ + public Sprite(int x, int y, SpriteTile tile, TileProperties tileProperties = 0) + { + X = x; + Y = y; + Tile = tile; + TileProperties = tileProperties; + } + + public int X + { + get; + set; + } + + public int Y + { + get; + set; + } + + public SpriteTile Tile + { + get; + set; + } + + public TileProperties TileProperties + { + get; + set; + } + + public static bool operator ==(Sprite left, Sprite right) + { + return left.Equals(right); + } + + public static bool operator !=(Sprite left, Sprite right) + { + return !(left == right); + } + + public bool Equals(Sprite other) + { + return X.Equals(other.X) + && Y.Equals(other.Y) + && Tile.Equals(other.Tile) + && TileProperties.Equals(other.TileProperties); + } + + public override bool Equals(object? obj) + { + return obj is Sprite other && Equals(other); + } + + public override int GetHashCode() + { + return HashCode.Combine(Tile, X, Y); + } + + public override string ToString() + { + return $"{Tile}:{X:X3},{Y:X3}"; + } +} diff --git a/src/Snes/SpriteTile.cs b/src/Snes/SpriteTile.cs new file mode 100644 index 0000000..408a931 --- /dev/null +++ b/src/Snes/SpriteTile.cs @@ -0,0 +1,88 @@ +// +// 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.Snes; + +using System; + +public struct SpriteTile : IEquatable +{ + public int TileIndex; + public int PaletteIndex; + public int Priority; + public bool FlipX; + public bool FlipY; + + public SpriteTile(int tileIndex, int paletteIndex, int priority = 0) + : this(tileIndex, paletteIndex, priority, false, false) + { } + + public SpriteTile( + int tileIndex, + int paletteIndex, + int priority, + bool flipX, + bool flipY) + { + TileIndex = tileIndex; + PaletteIndex = paletteIndex; + Priority = priority; + FlipX = flipX; + FlipY = flipY; + } + + public SpriteTile(ChrTile tile, int tileStartIndex) + { + TileIndex = tile.TileIndex + tileStartIndex; + PaletteIndex = tile.PaletteIndex + 8; + Priority = ((int)tile.Priority * 3) + 2; + FlipX = tile.XFlipped; + FlipY = tile.YFlipped; + } + + public SpriteTile(ObjTile tile, int tileStartIndex, int layer) + { + TileIndex = tile.TileIndex + tileStartIndex; + PaletteIndex = tile.PaletteIndex; + Priority = ((int)tile.Priority * 3) + layer; + FlipX = tile.XFlipped; + FlipY = tile.YFlipped; + } + + public static bool operator ==(SpriteTile left, SpriteTile right) + { + return left.Equals(right); + } + + public static bool operator !=(SpriteTile left, SpriteTile right) + { + return !(left == right); + } + + public bool Equals(SpriteTile other) + { + return TileIndex.Equals(other.TileIndex) + && PaletteIndex.Equals(other.PaletteIndex) + && Priority.Equals(other.Priority) + && FlipX.Equals(other.FlipX) + && FlipY.Equals(other.FlipY); + } + + public override bool Equals(object? obj) + { + return obj is SpriteTile other && Equals(other); + } + + public override int GetHashCode() + { + return HashCode.Combine(TileIndex, PaletteIndex, Priority, FlipX, FlipY); + } + + public override string ToString() + { + return $"{TileIndex:X4}"; + } +} diff --git a/src/Snes/TileFlip.cs b/src/Snes/TileFlip.cs new file mode 100644 index 0000000..956134f --- /dev/null +++ b/src/Snes/TileFlip.cs @@ -0,0 +1,18 @@ +// +// 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.Snes; + +using System; + +[Flags] +public enum TileFlip +{ + None = 0, + Horizontal = 1, + Veritcal = 2, + Both = Horizontal | Veritcal, +} diff --git a/src/Snes/TileProperties.cs b/src/Snes/TileProperties.cs new file mode 100644 index 0000000..3a9ccc9 --- /dev/null +++ b/src/Snes/TileProperties.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 Maseya.Snes; + +using System; + +[Flags] +public enum TileProperties +{ + None = 0, + Invert = 1 << 8, + Red = 1 << 9, + Green = 1 << 10, + Blue = 1 << 11, + Yellow = Red | Green, + Magenta = Red | Blue, + Cyan = Green | Blue, + White = Red | Green | Blue, + Transparent = 1 << 12, +} diff --git a/test/Brutario.Core.Tests/Brutario.Core.Tests.csproj b/test/Brutario.Core.Tests/Brutario.Core.Tests.csproj new file mode 100644 index 0000000..6aa12f6 --- /dev/null +++ b/test/Brutario.Core.Tests/Brutario.Core.Tests.csproj @@ -0,0 +1,29 @@ + + + + net8.0 + enable + + false + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + diff --git a/test/Brutario.Core.Tests/SortedObjectListEditorTests.cs b/test/Brutario.Core.Tests/SortedObjectListEditorTests.cs new file mode 100644 index 0000000..a492c4f --- /dev/null +++ b/test/Brutario.Core.Tests/SortedObjectListEditorTests.cs @@ -0,0 +1,83 @@ +namespace Brutario.Core.Tests; +using Maseya.Smas.Smb1.AreaData.ObjectData; + +using Xunit; + +public class SortedObjectListEditorTests +{ + [Fact] + public void TestObjectMove() + { + var objectData = new AreaObjectCommand[] + { + new AreaObjectCommand(0x00, 0x00), + new AreaObjectCommand(0x00, 0x01), + new AreaObjectCommand(0x00, 0x02), + new AreaObjectCommand(0x01, 0x03), + + new AreaObjectCommand(0x10, 0x04), + new AreaObjectCommand(0x11, 0x05), + new AreaObjectCommand(0x10, 0x06), + + new AreaObjectCommand(0x20, 0x07), + + new AreaObjectCommand(0x45, 0x08), + + new AreaObjectCommand(0x70, 0x89), + + new AreaObjectCommand(0x58, 0x0A), + }; + + var sortedObjectData = new SortedObjectListEditor(objectData); + Assert.True(false, "I switched to indexed data. So update the tests to use indices"); + /* + Assert.Equal( + expected: objectData.Length, + actual: sortedObjectData.Count); + + var nullableSelectedObject = sortedObjectData.AtCoords(0, 0); + + var selectedObject = nullableSelectedObject!.Value; + Assert.Equal( + expected: objectData[2], + actual: selectedObject.Command); + + var newObject = selectedObject; + newObject.X = 1; + sortedObjectData.Edit(selectedObject, newObject); + + nullableSelectedObject = sortedObjectData.AtCoords(1, 0); + + selectedObject = nullableSelectedObject!.Value; + Assert.Equal( + expected: newObject, + actual: selectedObject); + + nullableSelectedObject = sortedObjectData.AtCoords(0, 0); + + selectedObject = nullableSelectedObject!.Value; + Assert.Equal( + expected: objectData[1], + actual: selectedObject.Command); + + nullableSelectedObject = sortedObjectData.AtCoords(1, 0); + + selectedObject = nullableSelectedObject!.Value; + Assert.Equal( + expected: newObject, + actual: selectedObject); + + newObject = selectedObject; + newObject.X = 2; + + sortedObjectData.Edit(selectedObject, newObject); + + nullableSelectedObject = sortedObjectData.AtCoords(2, 0); + + selectedObject = nullableSelectedObject!.Value; + Assert.Equal( + expected: newObject, + actual: selectedObject); + */ + } +} \ No newline at end of file diff --git a/test/RedBlackTree.Tests/ExtensionMethods.cs b/test/RedBlackTree.Tests/ExtensionMethods.cs new file mode 100644 index 0000000..111edb8 --- /dev/null +++ b/test/RedBlackTree.Tests/ExtensionMethods.cs @@ -0,0 +1,92 @@ +namespace RedBlackTree.Tests; +using System.Collections.Generic; +using System.Linq; + +using Maseya; + +public static class ExtensionMethods +{ + public static bool IsSorted( + this IEnumerable collection, + IComparer? comparer = null) + { + comparer ??= Comparer.Default; + return collection.Zip(collection.Skip(1), comparer.Compare).All(x => x <= 0); + } + + public static int CountNodes( + this RedBlackTreeNode? node) + { + return node is not null + ? 1 + node.Left.CountNodes() + node.Right.CountNodes() + : 0; + } + + public static bool IsValidTree( + this RedBlackTreeNode? node) + { + if (node is null) + { + return true; + } + + if (node.Parent is not null) + { + if (node.Parent.Left != node && node.Parent.Right != node) + { + return false; + } + } + + return node.Left.IsValidTree() && node.Right.IsValidTree(); + } + + public static bool IsBalanced( + this RedBlackTreeNode? node) + { + var blackHeight = node.BlackHeight(); + return IsBalanced(node, 0, blackHeight); + } + + private static int BlackHeight( + this RedBlackTreeNode? node) + { + var result = 0; + while (node != null) + { + if (node.IsBlack()) + { + result++; + } + + node = node.Left; + } + + return result; + } + + private static bool IsBalanced( + RedBlackTreeNode? node, + int currentHeight, + int blackHeight) + { + if (node is null) + { + // Forbid black violation + return currentHeight == blackHeight; + } + + if (node.IsBlack()) + { + currentHeight++; + } + else if (node.Parent.IsRed()) + { + // Forbid red-violations. + return false; + } + + return IsBalanced(node.Left, currentHeight, blackHeight) + && IsBalanced(node.Right, currentHeight, blackHeight); + } +} diff --git a/test/RedBlackTree.Tests/RedBlackTree.Tests.csproj b/test/RedBlackTree.Tests/RedBlackTree.Tests.csproj new file mode 100644 index 0000000..8b80b9c --- /dev/null +++ b/test/RedBlackTree.Tests/RedBlackTree.Tests.csproj @@ -0,0 +1,27 @@ + + + + net6.0 + enable + + false + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + diff --git a/test/RedBlackTree.Tests/Sample.cs b/test/RedBlackTree.Tests/Sample.cs new file mode 100644 index 0000000..0f3916e --- /dev/null +++ b/test/RedBlackTree.Tests/Sample.cs @@ -0,0 +1,43 @@ +namespace RedBlackTree.Tests; +using System; +using System.Collections.Generic; +using System.Linq; + +public static class Sample +{ + public static IEnumerable RandomSample(int count, bool distinct) + { + return RandomSample(count, new Random(), distinct); + } + + public static IEnumerable RandomSample(int count, int seed, bool distinct) + { + return RandomSample(count, new Random(seed), distinct); + } + + public static IEnumerable RandomSample( + int count, + Random random, + bool distinct) + { + if (!distinct) + { + for (var i = 0; i < count; i++) + { + yield return random.Next(count); + } + } + else + { + var indexes = Enumerable.Range(0, count).ToArray(); + for (var i = 0; i < count; i++) + { + var index = random.Next(count - i); + yield return indexes[index]; + + (indexes[index], indexes[count - i - 1]) = + (indexes[count - i - 1], indexes[index]); + } + } + } +} diff --git a/test/RedBlackTree.Tests/TestRedBlackTree.cs b/test/RedBlackTree.Tests/TestRedBlackTree.cs new file mode 100644 index 0000000..eb84236 --- /dev/null +++ b/test/RedBlackTree.Tests/TestRedBlackTree.cs @@ -0,0 +1,179 @@ +namespace RedBlackTree.Tests; + +using System; +using System.Collections.Generic; +using System.Linq; + +using Maseya; + +using Xunit; + +using static Sample; + +public class TestRedBlackTree +{ + [Fact] + public void TreeStressTests() + { + var tree = new RedBlackTree(); + for (var count = 0; count < 50; count++) + { + for (var seed = 0; seed < 500; seed++) + { + var random = new Random(seed); + var sample = RandomSample( + count, + random: random +, + distinct: true).ToArray(); + + for (var i = 0; i < count; i++) + { + tree.Add(sample[i], i); + Assert.Equal(i + 1, tree.Count); + AssertTree(tree); + } + + Assert.Equal(count, tree.Count); + + var en = RandomSample(count, random: random, distinct: true) + .Zip(Enumerable.Range(0, count)); + foreach ((var key, var i) in en) + { + if (!tree.Remove(key)) + { + throw new InvalidOperationException(); + } + + Assert.Equal(count - 1 - i, tree.Count); + AssertTree(tree); + } + + Assert.Empty(tree); + } + } + } + + [Fact] + public void TestLowerBound() + { + var random = new Random(0); + for (var i = 0; i < 1000; i++) + { + var tree = new RedBlackTree(); + for (var j = 0; j < random.Next(1000); j++) + { + tree.Add(random.Next(1000), j); + } + + var key = random.Next(1000); + var lower = tree.LowerBound(key); + if (lower is not null) + { + Assert.True(lower.Key >= key); + Assert.True(lower.Prev is null || lower.Prev.Key < key); + } + else + { + Assert.True(tree.Root is null || tree.Root.Max().Key < key); + } + } + } + + [Fact] + public void TestUpperBound() + { + var random = new Random(0); + for (var i = 0; i < 1000; i++) + { + var tree = new RedBlackTree(); + for (var j = 0; j < random.Next(1000); j++) + { + tree.Add(random.Next(1000), j); + } + + var key = random.Next(1000); + var lower = tree.UpperBound(key); + if (lower is not null) + { + Assert.True(lower.Key > key); + Assert.True(lower.Prev is null || lower.Prev.Key <= key); + } + else + { + Assert.True(tree.Root is null || tree.Root.Max().Key <= key); + } + } + } + + [Fact] + public void TestNext() + { + var tree = new RedBlackTree(); + for (var count = 0; count < 50; count++) + { + for (var seed = 0; seed < 100; seed++) + { + var random = new Random(seed); + var sample = RandomSample( + count, + random: random +, + distinct: true).ToArray(); + + for (var i = 0; i < count; i++) + { + tree.Add(sample[i], i); + AssertSuccessors(tree); + } + + foreach (var key in RandomSample(count, random: random, distinct: true)) + { + _ = tree.Remove(key); + AssertSuccessors(tree); + } + } + } + } + + private static void AssertTree(RedBlackTree tree) + { + Assert.True(tree.Select(x => x.Key).IsSorted()); + + Assert.Equal(expected: tree.Root.CountNodes(), actual: tree.Count); + + Assert.True(tree.Root.IsValidTree()); + + Assert.True(tree.Root.IsBalanced()); + } + + private static void AssertSuccessors(RedBlackTree tree) + { + if (tree.Root is null) + { + return; + } + + var nodes = tree.Root.InOrderTraversal().ToArray(); + for (var j = 0; j < nodes.Length; j++) + { + if (j > 0) + { + Assert.Equal(nodes[j - 1], nodes[j].Prev); + } + else + { + Assert.Null(nodes[j].Prev); + } + + if (j < nodes.Length - 1) + { + Assert.Equal(nodes[j + 1], nodes[j].Next); + } + else + { + Assert.Null(nodes[j].Next); + } + } + } +} diff --git a/test/Snes.Tests/Properties/Resources.Designer.cs b/test/Snes.Tests/Properties/Resources.Designer.cs new file mode 100644 index 0000000..467036d --- /dev/null +++ b/test/Snes.Tests/Properties/Resources.Designer.cs @@ -0,0 +1,94 @@ +//------------------------------------------------------------------------------ +// +// 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 Maseya.Snes.Tests.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("Snes.Tests.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 string similar to [ + /// { + /// "AddressMode": "LoRom", + /// "FileName": "Aladdin (E)", + /// "HiRom": "AACqAP//f5/HgP//v5+/qr+fn5+eqgD//8DAqgDAwMDBOP8AAAAC/wCgAP//AgD/zv/9AQAAAH8F4jXSLcKgTYoA/v//IP/vfwEciZQAFgU=", + /// "LoRom": "XOmIgAADAAEB8BcAAA8Pf0FMQURESU4gICAgICAgICAgICAgIDAACwACCAA88MMPRWcyM7D/rP+s/6T/rP+o/1gsIC+w/6z/rP+s/wCArP8=", + /// "Size": 1310720 + /// }, + /// { + /// "AddressMode": "LoRom", + /// "FileName": "Aladdin (F)", + /// "HiRom": "AAAAAAAAAAAAAAAAAAAAAEACkggOAIhaggI6ABcAliQKYEciOIlDgqAAg0AVyA0STFQCSAoIAQJIgdgiKYBC [rest of string was truncated]";. + /// + internal static string RomFormatTestData + { + get + { + return ResourceManager.GetString("RomFormatTestData", resourceCulture); + } + } + } +} diff --git a/test/Snes.Tests/Properties/Resources.resx b/test/Snes.Tests/Properties/Resources.resx new file mode 100644 index 0000000..77120b0 --- /dev/null +++ b/test/Snes.Tests/Properties/Resources.resx @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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\RomFormatTestData.json;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/test/Snes.Tests/Resources/RomFormatTestData.json b/test/Snes.Tests/Resources/RomFormatTestData.json new file mode 100644 index 0000000..c9ff6bc --- /dev/null +++ b/test/Snes.Tests/Resources/RomFormatTestData.json @@ -0,0 +1,1682 @@ +{ + "Aladdin (E)": { + "AddressMode": "LoRom", + "Size": 1310720, + "LoRom": "XOmIgAADAAEB8BcAAA8Pf0FMQURESU4gICAgICAgICAgICAgIDAACwACCAA88MMPRWcyM7D/rP+s/6T/rP+o/1gsIC+w/6z/rP+s/wCArP8=", + "HiRom": "AACqAP//f5/HgP//v5+/qr+fn5+eqgD//8DAqgDAwMDBOP8AAAAC/wCgAP//AgD/zv/9AQAAAH8F4jXSLcKgTYoA/v//IP/vfwEciZQAFgU=", + "ExLoRom": null, + "ExHiRom": null + }, + "Aladdin (F)": { + "AddressMode": "LoRom", + "Size": 1310720, + "LoRom": "XOmIgAAAAAAAAAAAAAAAAEFMQURESU4gICAgICAgICAgICAgIDAACwAGCAA3eMiHAAwwqLD/rP+s/6T/rP+o/7pz0W2w/6z/rP+s/wCArP8=", + "HiRom": "AAAAAAAAAAAAAAAAAAAAAEACkggOAIhaggI6ABcAliQKYEciOIlDgqAAg0AVyA0STFQCSAoIAQJIgdgiKYBCIJ+M+7nt7bkYl6znmd3YGRs=", + "ExLoRom": null, + "ExHiRom": null + }, + "Aladdin (G) [!]": { + "AddressMode": "LoRom", + "Size": 1310720, + "LoRom": "XOmIgDBAVR9gYGBgl//AAEFMQURESU4gICAgICAgICAgICAgIDAACwAJCAAqH9XgRvZi6rD/rP+s/6T/rP+o/w0E/TCw/6z/rP+s/wCArP8=", + "HiRom": "GH+XZikRxAgywAoAgOCqAPj+//8AAG+AAMDgwHBgAAACAID6f/55+HfwsH94t1j/KF+UT6oA/vjw8KoAeDg8PH8A/D7gGfiwkPMjeDiMDDA=", + "ExLoRom": null, + "ExHiRom": null + }, + "Aladdin (J)": { + "AddressMode": "LoRom", + "Size": 1310720, + "LoRom": "XOmIgAAAAAAAAIAAAAAAAEFMQURESU4gICAgICAgICAgICAgIDAACwAACABlpJpbAAAAALD/rP+s/6T/rP+o/wAAAACw/6z/rP+s/wCArP8=", + "HiRom": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAABAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", + "ExLoRom": null, + "ExHiRom": null + }, + "Aladdin (U) [!]": { + "AddressMode": "LoRom", + "Size": 1310720, + "LoRom": "XOmIgAAAYJ9/Ym9GV15/bEFMQURESU4gICAgICAgICAgICAgIDAACwABCAD1+QoGf3E4GbD/rP+s/6T/rP+o//RVOjiw/6z/rP+s/wCArP8=", + "HiRom": "AIDgqgD4/v//AABvgADA4MBwYAAAAgCA+n/+efh38LB/eLdY/yhflE+qAP748PCqAHg4PDx/APw+4Bn4sJDzI3g4jAwwACoAHgdhqgDAwMA=", + "ExLoRom": null, + "ExHiRom": null + }, + "Arkanoid - Doh It Again (U) [!]": { + "AddressMode": "LoRom", + "Size": 524288, + "LoRom": "MDFBNiAgAAAAAAAAAAAAAEFSS0FOT0lEIERPSCBJVCBBR0FJTjAACQABMwDyng1hAAAAAGiAiYAzgaqAAIATgQAAAAAzgTOBM4EzgQCAM4E=", + "HiRom": "//////////////////////////////////////////////////////////////////////////////////////////////////////////8=", + "ExLoRom": null, + "ExHiRom": null + }, + "Axelay (U)": { + "AddressMode": "LoRom", + "Size": 1048576, + "LoRom": "/////////////////////0FYRUxBWSAgICAgICAgICAgICAgICAACgABpADHvDhD/////16NXo1eja6NnIsgjv////9ejV6NXo1ejZyLXo0=", + "HiRom": "/////////////////////////////////////////////////////////////////////////////////////////////////////////wE=", + "ExLoRom": null, + "ExHiRom": null + }, + "Battletoads & Double Dragon - The Ultimate Team (U) [!]": { + "AddressMode": "LoRom", + "Size": 1048576, + "LoRom": "jQAhwiBsBAB6+mhAeEAJcEJBVFRMRVRPQURTIEQuRC4gICAgIDAACgABXQC810MoUkFSRTkzA3AAAH7/AACY/01BVFQgMTk5MyEA+ACAAHA=", + "HiRom": "Ia3fB41BIcIgYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", + "ExLoRom": null, + "ExHiRom": null + }, + "BS Mario Collection 3 (J)": { + "AddressMode": "LoRom", + "Size": 1048576, + "LoRom": "MDEAAAAAAAAAAAAAAAAAAEJTz9i1utq4vK7dIDOPVCD/AAAAAIAwiDAAMwIezeEyAAAAAKD/oP+g/6H/pf+p/wECAACg/6D/oP+g/6X/qf8=", + "HiRom": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAAACAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAA=", + "ExLoRom": null, + "ExHiRom": null + }, + "Chrono Trigger (U) [!]": { + "AddressMode": "HiRom", + "Size": 4194304, + "LoRom": "hZ+YGGV8haGlXhhlfIWjpWyF26Vuhd2laoXfpZoYaRwAKR8AqGggoYEYZXyFp4oYZXyFpZgYZXyFqaWaOikfAEiopZkpPwAg1oGKGGV8has=", + "HiRom": "QzNBQ1RFAAAAAAAAAAAAAENIUk9OTyBUUklHR0VSICAgICAgIDECDAMBMwBzh4x4////////GP///xD///8U//////8Y/////////wD///8=", + "ExLoRom": null, + "ExHiRom": null + }, + "Contra III - The Alien Wars (U) [!]": { + "AddressMode": "LoRom", + "Size": 1048576, + "LoRom": "Fmv//////////////////0NPTlRSQTMgVEhFIEFMSUVOIFdBUlMACgABpADD8zwM/////+KL4ovii5uBAIDii//////ii+KL4oubgQCA4os=", + "HiRom": "//////////////////////////////////////////////////////////////////////////////////////////////////////////8=", + "ExLoRom": null, + "ExHiRom": null + }, + "Contra Spirits (J)": { + "AddressMode": "LoRom", + "Size": 1048576, + "LoRom": "/////////////////////0NPTlRSQSBTUElSSVRTICAgICAgICAACgAAygBWYame/////+KL4ovii5uBAIDii//////ii+KL4oubgQCA4os=", + "HiRom": "//////////////////////////////////////////////////////////////////////////////////////////////////////////8=", + "ExLoRom": null, + "ExHiRom": null + }, + "Super Probotector - The Alien Rebels (E)": { + "AddressMode": "LoRom", + "Size": 1048576, + "LoRom": "/////////////////////1NVUEVSIFBST0JPVEVDVE9SICAgICAACgACpAA32cgm/////7qLuou6i5uBAIC6i/////+6i7qLuoubgQCAuos=", + "HiRom": "//////////////////////////////////////////////////////////////////////////////////////////////////////////8=", + "ExLoRom": null, + "ExHiRom": null + }, + "Daikaijuu Monogatari II (Japan)": { + "AddressMode": "ExHiRom", + "Size": 5242880, + "LoRom": "ruUDysowCr0TBIVIIOZ/gPKulQPKyjAKvZsDhUgg5n+A8q7DA8rKMAq9xQOFSCDmf4DyevprwiDaoAoAsUiJAEDwBSC7gIApiQAC0AUgYoA=", + "HiRom": "//////////////////////////////////////////////////////////////////////////////////////////////////////////8=", + "ExLoRom": "AI8vk3eTs5bXlteW96D3oDKrMqsUAKBWAf4AAAP9AAAC3QAAAt0AAAJjAAAAAP//DACgVQCPzpbplgqaHJocmgwAoFYC/QAAAf4AAAJjAAA=", + "ExHiRom": "MThBRTZKAAAAAAAAAAAAAERBSUtBSUpZVU1PTk9HQVRBUkkyIDVVDQMAMwDXeiiFAAAAAAAAAAAAAJz/AACg/wAAAAAAAAAAAACc/4D/oP8=" + }, + "Demon's Blazon - Makai-Mura Monshou Hen (J)": { + "AddressMode": "LoRom", + "Size": 2097152, + "LoRom": "RbJMgofAUvhBoUk6hnZM22RlbW9uJ3MgYmxhem9uICAgICAgIDAACwAACACC0n0t3p1DPKz/rP+s/6T/rP+o/46saHes/6z/rP+s/4//rP8=", + "HiRom": "0Yp/lFR6P1yCaawXgctI+VyoFlAnVUucASgBkIAMAqgIpi1lofcRtKNBS4uFzPu7nRZ0WHIHl/jGG4NiQJQQWOYW9Rp4QZUB2tgxkJMq1bc=", + "ExLoRom": null, + "ExHiRom": null + }, + "Demon's Crest (E)": { + "AddressMode": "LoRom", + "Size": 2097152, + "LoRom": "MDgzWiAgAAAAAAAAAAAAAGRlbW9uJ3MgY3Jlc3QgICAgICAgIDAACwACMwCmxFk7/59DfKz/rP+s/6T/rP+o/96u7Hes/6z/rP+s/4//rP8=", + "HiRom": "0cv/lF56P16CeawXxetI+Vy9tnQ3VducASgJsKAMsqgo5y1l6/dVvOPJa6uVzfu/ndZ0XnoH1/zmH4PrwJR42u4W9xp4QdWD/tgxlNMq17c=", + "ExLoRom": null, + "ExHiRom": null + }, + "Demon's Crest (U) [!]": { + "AddressMode": "LoRom", + "Size": 2097152, + "LoRom": "MDgzWiAgAAAAAAAAAAAAAGRlbW9uJ3MgY3Jlc3QgICAgICAgIDAACwABMwCCO33E3p1DPKz/rP+s/6T/rP+o/46ubHes/6z/rP+s/4//rP8=", + "HiRom": "0Yp/lFR6P1yCeaQXwctI+VypllAnVUOcASgBkIAMEqgI5i1lqfcRtKNBa6uFzPu7nFZ0WHIHl/jGG4NiQJQQWuYW9Rp4QZUB+tgxkNMq1bc=", + "ExLoRom": null, + "ExHiRom": null + }, + "Donkey Kong Country (E) (V1.0) [!]": { + "AddressMode": "HiRom", + "Size": 4194304, + "LoRom": "bwNWDggYGzadX/9/P69f/wD/Af8H/w/4PuE4x3CPQL8wANGQAgA4X68Bce0AAAAAAAAAAAEAAEAgACPOAQDQPyABjZABADZAIAGNkAEAtD8=", + "HiRom": "MDE4WCAgAAAAAAAAAAAAAERPTktFWSBLT05HIENPVU5UUlkgIDECDAECMwD2XgmhRElERFkgA3AAAGKpAACKqURPTktFWUtPTkcA+ACAAHA=", + "ExLoRom": null, + "ExHiRom": null + }, + "Donkey Kong Country (E) (V1.1) [!]": { + "AddressMode": "HiRom", + "Size": 4194304, + "LoRom": "bwNWDggYGzadX/9/P69f/wD/Af8H/w/4PuE4x3CPQL8wANGQAgA4X68Bce0AAAAAAAAAAAEAAEAgACPOAQDQPyABjZABADZAIAGNkAEAtD8=", + "HiRom": "MDE4WCAgAAAAAAAAAAAAAERPTktFWSBLT05HIENPVU5UUlkgIDECDAECMwFIrLdTRElERFkgA3AAAGKpAACKqURPTktFWUtPTkcA+ACAAHA=", + "ExLoRom": null, + "ExHiRom": null + }, + "Donkey Kong Country (U) (V1.0) [!]": { + "AddressMode": "HiRom", + "Size": 4194304, + "LoRom": "bwNWDggYGzadX/9/P69f/wD/Af8H/w/4PuE4x3CPQL8wANGQAgA4X68Bce0AAAAAAAAAAAEAAEAgACPOAQDQPyABjZABADZAIAGNkAEAtD8=", + "HiRom": "MDE4WCAgAAAAAAAAAAAAAERPTktFWSBLT05HIENPVU5UUlkgIDECDAEBMwB/EIDvRElERFkgA3AAAGipAACQqURPTktFWUtPTkcA+ACAAHA=", + "ExLoRom": null, + "ExHiRom": null + }, + "Donkey Kong Country (U) (V1.1)": { + "AddressMode": "HiRom", + "Size": 4194304, + "LoRom": "bwNWDggYGzadX/9/P69f/wD/Af8H/w/4PuE4x3CPQL8wANGQAgA4X68Bce0AAAAAAAAAAAEAAEAgACPOAQDQPyABjZABADZAIAGNkAEAtD8=", + "HiRom": "MDE4WCAgAAAAAAAAAAAAAERPTktFWSBLT05HIENPVU5UUlkgIDECDAEBMwGDLnzRRElERFkgA3AAAF+pAACHqURPTktFWUtPTkcA+ACAAHA=", + "ExLoRom": null, + "ExHiRom": null + }, + "Donkey Kong Country (U) (V1.2) [!]": { + "AddressMode": "HiRom", + "Size": 4194304, + "LoRom": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", + "HiRom": "MDE4WCAgAAAAAAAAAAAAAERPTktFWSBLT05HIENPVU5UUlkgIDECDAEBMwIz1MwrRElERFkgA3AAAHapAACeqURPTktFWUtPTkcA+ACAAHA=", + "ExLoRom": null, + "ExHiRom": null + }, + "Donkey Kong Country - Competition Cartridge (U)": { + "AddressMode": "HiRom", + "Size": 4194304, + "LoRom": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", + "HiRom": "MDE4RSAgAAAAAAAAAAAAAERPTktFWSBLT05HIENPVU5UUlkgIDECDAEBMwBlrppRRElERFkgA3AAANWpAAD9qURPTktFWUtPTkcA+ACAAHA=", + "ExLoRom": null, + "ExHiRom": null + }, + "Super Donkey Kong (J) (V1.0)": { + "AddressMode": "HiRom", + "Size": 4194304, + "LoRom": "bwNWDggYGzadX/9/P69f/wD/Af8H/w/4PuE4x3CPQL8wANGQAgA4X68Bce0AAAAAAAAAAAEAAEAgACPOAQDQPyABjZABADZAIAGNkAEAtD8=", + "HiRom": "MDE4WCAgAAAAAAAAAAAAAFNVUEVSIERPTktFWSBLT05HICAgIDECDAEAMwDo8xcMRElERFkgA3AAAJKpAAC6qURPTktFWUtPTkcA+ACAAHA=", + "ExLoRom": null, + "ExHiRom": null + }, + "Super Donkey Kong (J) (V1.1)": { + "AddressMode": "HiRom", + "Size": 4194304, + "LoRom": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", + "HiRom": "MDE4WCAgAAAAAAAAAAAAAFNVUEVSIERPTktFWSBLT05HICAgIDECDAEAMwGmf1mARElERFkgA3AAAJKpAAC6qURPTktFWUtPTkcA+ACAAHA=", + "ExLoRom": null, + "ExHiRom": null + }, + "Donkey Kong Country 2 - Diddy's Kong Quest (E) (V1.1) [!]": { + "AddressMode": "HiRom", + "Size": 4194304, + "LoRom": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABk/qsM=", + "HiRom": "MDFBRE5QAAAAAAAAAAAAAERJRERZJ1MgS09ORyBRVUVTVCAgIDECDAECMwGx5U4aRElERFkgA3AAAL3zAAD580RJRERZIEtPTkcA+PeDAHA=", + "ExLoRom": null, + "ExHiRom": null + }, + "Donkey Kong Country 2 - Diddy's Kong Quest (G) (V1.0)": { + "AddressMode": "HiRom", + "Size": 4194304, + "LoRom": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKrDPxk=", + "HiRom": "MDFBRE5EAAAAAAAAAAAAAERJRERZJ1MgS09ORyBRVUVTVCAgIDECDAEJMwBGl7loRElERFkgA3AAAH3zAAC580RJRERZIEtPTkcA+PeDAHA=", + "ExLoRom": null, + "ExHiRom": null + }, + "Donkey Kong Country 2 - Diddy's Kong Quest (G) (V1.1) [!]": { + "AddressMode": "HiRom", + "Size": 4194304, + "LoRom": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABk/qsM=", + "HiRom": "MDFBRE5EAAAAAAAAAAAAAERJRERZJ1MgS09ORyBRVUVTVCAgIDECDAEJMwF5pIZbRElERFkgA3AAAL3zAAD580RJRERZIEtPTkcA+PeDAHA=", + "ExLoRom": null, + "ExHiRom": null + }, + "Donkey Kong Country 2 - Diddy's Kong Quest (U) (V1.0)": { + "AddressMode": "HiRom", + "Size": 4194304, + "LoRom": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKrDPxk=", + "HiRom": "MDFBRE5FAAAAAAAAAAAAAERJRERZJ1MgS09ORyBRVUVTVCAgIDECDAEBMwD97QISRElERFkgA3AAAH3zAAC580RJRERZIEtPTkcA+PeDAHA=", + "ExLoRom": null, + "ExHiRom": null + }, + "Donkey Kong Country 2 - Diddy's Kong Quest (U) (V1.1) [!]": { + "AddressMode": "HiRom", + "Size": 4194304, + "LoRom": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABk/qsM=", + "HiRom": "MDFBRE5FAAAAAAAAAAAAAERJRERZJ1MgS09ORyBRVUVTVCAgIDECDAEBMwGfZ2CYRElERFkgA3AAAL3zAAD580RJRERZIEtPTkcA+PeDAHA=", + "ExLoRom": null, + "ExHiRom": null + }, + "Super Donkey Kong 2 - Dixie & Diddy (J) (V1.0)": { + "AddressMode": "HiRom", + "Size": 4194304, + "LoRom": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKrDPxk=", + "HiRom": "MDFBRE5KAAAAAAAAAAAAAFNVUEVSIERPTktFWSBLT05HIDIgIDECDAEAMwCHA3j8RElERFkgA3AAAEHzAAB980RJRERZIEtPTkcA+PeDAHA=", + "ExLoRom": null, + "ExHiRom": null + }, + "Super Donkey Kong 2 - Dixie & Diddy (J) (V1.1)": { + "AddressMode": "HiRom", + "Size": 4194304, + "LoRom": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKrDPxk=", + "HiRom": "MDFBRE5KAAAAAAAAAAAAAFNVUEVSIERPTktFWSBLT05HIDIgIDECDAEAMwExys41RElERFkgA3AAAIHzAAC980RJRERZIEtPTkcA+PeDAHA=", + "ExLoRom": null, + "ExHiRom": null + }, + "Donkey Kong Country 3 - Dixie Kong's Double Trouble (E) [!]": { + "AddressMode": "HiRom", + "Size": 4194304, + "LoRom": "/////////////////////////////////////////////////////////////////////////////////////////////////////6rDPxk=", + "HiRom": "MDFBM0NQAAAAAAAAAAAAAERPTktFWSBLT05HIENPVU5UUlkgMzECDAECMwD8DQPyRElYSUUgA1AAAHzKAAClyiAgICAgICAgICAA+MSAAFA=", + "ExLoRom": null, + "ExHiRom": null + }, + "Donkey Kong Country 3 - Dixie Kong's Double Trouble (U) [!]": { + "AddressMode": "HiRom", + "Size": 4194304, + "LoRom": "/////////////////////////////////////////////////////////////////////////////////////////////////////6rDPxk=", + "HiRom": "MDFBM0NFAAAAAAAAAAAAAERPTktFWSBLT05HIENPVU5UUlkgMzECDAEBMwBzTYyyRElYSUUgA1AAAEXKAABuyiAgICAgICAgICAA+MSAAFA=", + "ExLoRom": null, + "ExHiRom": null + }, + "Super Donkey Kong 3 - Nazo no Krems Shima (J) (V1.0)": { + "AddressMode": "HiRom", + "Size": 4194304, + "LoRom": "/////////////////////////////////////////////////////////////////////////////////////////////////////6rDPxk=", + "HiRom": "MDFBM0NKAAAAAAAAAAAAAFNVUEVSIERPTktFWSBLT05HIDMgIDECDAEAMwC6GkXlRElYSUUgA1AAAIvJAAC0ySAgICAgICAgICAA+MSAAFA=", + "ExLoRom": null, + "ExHiRom": null + }, + "Super Donkey Kong 3 - Nazo no Krems Shima (J) (V1.1)": { + "AddressMode": "HiRom", + "Size": 4194304, + "LoRom": "AAAACCAQAAAUAAgAAAAAAAAAAAAQMAgQEBgIEAAAAAAAAAAgMAAABBAAEABVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVarDPxk=", + "HiRom": "MDFBM0NKAAAAAAAAAAAAAFNVUEVSIERPTktFWSBLT05HIDMgIDECDAEAMwEGR/m4RElYSUUgA1AAAK3JAADWySAgICAgICAgICAA+MSAAFA=", + "ExLoRom": null, + "ExHiRom": null + }, + "Earthbound (U)": { + "AddressMode": "HiRom", + "Size": 3145728, + "LoRom": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", + "HiRom": "MDFNQiAgAAAAAAAAAAAAAEVBUlRIIEJPVU5EICAgICAgICAgIDECDAMBMwC3v0hAAAAAAP9f/1//X0eBAABLgQAAAAD/XwAA/1//X0GB/18=", + "ExLoRom": null, + "ExHiRom": null + }, + "Mother 2 (J)": { + "AddressMode": "HiRom", + "Size": 3145728, + "LoRom": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", + "HiRom": "MDFNQiAgAAAAAAAAAAAAAE1PVEhFUi0yICAgICAgICAgICAgIDECDAMAMwA8XcOiAAAAAP9f/1//X0eBAABLgQAAAAD/XwAA/1//X0OB/18=", + "ExLoRom": null, + "ExHiRom": null + }, + "F-ZERO (E)": { + "AddressMode": "LoRom", + "Size": 524288, + "LoRom": "/////////////////////0YtWkVSTyAgICAgICAgICAgICAgICACCQECAQADPfzCAAAAADiBOIE4gdmA//8BhgAAAAA4gf//OIE4gQCAOIE=", + "HiRom": "NndTegAP/w8RM0MzeiIiIzIP/rq7iu7d7t3gE0Qyei/+Dv8RM0QzeiITMyIQ7burit7u3t3QE0UxeiEN/g8BM0RDeiEjMzIQ7cuqit7u3t0=", + "ExLoRom": null, + "ExHiRom": null + }, + "F-ZERO (J)": { + "AddressMode": "LoRom", + "Size": 524288, + "LoRom": "/////////////////////0YtWkVSTyAgICAgICAgICAgICAgICABCQEAAQBFObrG/////ziBslA4gdmA//8Bhv////84gf//OIE4gQCAOIE=", + "HiRom": "DC+EDH+QhAYvhAZ/kAYvhAZ/kAyEiZWJlYkGk5WPkAyJDC+ODH+ajgaYmgzJBoyOmJoMjIQGj5DJiovJGIQGmpwMkAAGf8oMzgbKDM0Gzs4=", + "ExLoRom": null, + "ExHiRom": null + }, + "F-ZERO (U) [!]": { + "AddressMode": "LoRom", + "Size": 524288, + "LoRom": "/////////////////////0YtWkVSTyAgICAgICAgICAgICAgICACCQEBAQDRNi7J/////ziBOIE4gdmA//8Bhv////84gf//OIE4gQCAOIE=", + "HiRom": "DC+EDH+QhAYvhAZ/kAYvhAZ/kAyEiZWJlYkGk5WPkAyJDC+ODH+ajgaYmgzJBoyOmJoMjIQGj5DJiovJGIQGmpwMkAAGf8oMzgbKDM0Gzs4=", + "ExLoRom": null, + "ExHiRom": null + }, + "Final Fantasy - Mystic Quest (U) (V1.0) [!]": { + "AddressMode": "LoRom", + "Size": 524288, + "LoRom": "/////////////////////0ZGIE1ZU1RJQyBRVUVTVCAgICAgICACCQMBwwAhs95M////////GwEAgBMB//8XAf///////////////wCA//8=", + "HiRom": "GGlAAI0UGhhpIACNFhriIGCtiQ446QiNLRmtig446QaNLhmiDwCOvxmiAACOvRnaIHf5IL6D7i4Z+o6/GejgDQDQ7KIAAI6/GWD///////8=", + "ExLoRom": null, + "ExHiRom": null + }, + "Final Fantasy - Mystic Quest (U) (V1.1)": { + "AddressMode": "LoRom", + "Size": 524288, + "LoRom": "/////////////////////0ZGIE1ZU1RJQyBRVUVTVCAgICAgICACCQMBwwEKcPWP////////GwEAgBMB//8XAf///////////////wCA//8=", + "HiRom": "GhhpQACNFBoYaSAAjRYa4iBgrYkOOOkIjS0ZrYoOOOkGjS4Zog8Ajr8ZogAAjr0Z2iB4+SC/g+4uGfqOvxno4A0A0OyiAACOvxlg//////8=", + "ExLoRom": null, + "ExHiRom": null + }, + "Final Fantasy USA - Mystic Quest (J)": { + "AddressMode": "LoRom", + "Size": 524288, + "LoRom": "/////////////////////0ZGIE1ZU1RJQyBRVUVTVCAgICAgICACCQMAwwBt+5IE////////GwEAgBMB//8XAf///////////////wCA//8=", + "HiRom": "wiCtFBoYaUAAjRQaGGkgAI0WGuIgYK2JDjjpCI0tGa2KDjjpBo0uGaIPAI6/GaIAAI69GdogfPkgv4PuLhn6jr8Z6OANANDsogAAjr8ZYP8=", + "ExLoRom": null, + "ExHiRom": null + }, + "Mystic Quest Legend (E)": { + "AddressMode": "LoRom", + "Size": 524288, + "LoRom": "AAAAAAAAAAAAAAAAAAAAAE1ZU1RJQyBRVUVTVCBMRUdFTkQgICACCQMCAQDWLSnSAEAAAGASGwEAgBMBAEAXAYAIIWiTBOZS6AoB8ACACAA=", + "HiRom": "GhhpQACNFBoYaSAAjRYa4iBgrYkOOOkIjS0ZrYoOOOkGjS4Zog8Ajr8ZogAAjr0Z2iB4+SC/g+4uGfqOvxno4A0A0OyiAACOvxlgjr8ZYIA=", + "ExLoRom": null, + "ExHiRom": null + }, + "Mystic Quest Legend (F)": { + "AddressMode": "LoRom", + "Size": 524288, + "LoRom": "AAAAAAAAAAAAAAAAAAAAAE1ZU1RJQyBRVUVTVCBMRUdFTkQgICACCQMGAQCAGH/nAEAAAAAAGwEAgBMBAAAXAQAAIAACAIACwAgB8ACAAAA=", + "HiRom": "wiCtFBoYaUAAjRQaGGkgAI0WGuIgYK2JDjjpCI0tGa2KDjjpBo0uGaIPAI6/GaIAAI69GdogfPkgv4PuLhn6jr8Z6OANANDsogAAjr8ZYAA=", + "ExLoRom": null, + "ExHiRom": null + }, + "Mystic Quest Legend (G)": { + "AddressMode": "LoRom", + "Size": 524288, + "LoRom": "AAAAAAAAAAAAAAAAAAAAAE1ZU1RJQyBRVUVTVCBMRUdFTkQgICACCQMJAQDO4zEcEEAAACASGwEAgBMBAEAXAYAIIWiTBOZS6AoB8ACACAA=", + "HiRom": "wiCtFBoYaUAAjRQaGGkgAI0WGuIgYK2JDjjpCI0tGa2KDjjpBo0uGaIPAI6/GaIAAI69GdogfPkgv4PuLhn6jr8Z6OANANDsogAAjr8ZYIA=", + "ExLoRom": null, + "ExHiRom": null + }, + "FF4FE.bAAMI_KkGAMAF4kA6JQ.6DYM4S5PAX": { + "AddressMode": "LoRom", + "Size": 2097152, + "LoRom": "///////0uv9s+w9rINCSa0ZGSVY6IEZSRUUgRU5URVJQUklTRSACCwUBwwFJUbau/////////////wAC//8EAv///////////////wCA//8=", + "HiRom": "////////////////////////////////////////////////////////////////////////////////////////////////9P7/bPsP/2s=", + "ExLoRom": null, + "ExHiRom": null + }, + "Final Fantasy II (U) (V1.0) [!]": { + "AddressMode": "LoRom", + "Size": 1048576, + "LoRom": "////////////////INCSa0ZJTkFMIEZBTlRBU1kgSUkgICAgICACCgMBwwAPevCF/////////////wAC//8EAv///////////////wCA//8=", + "HiRom": "//////////////////////////////////////////////////////////////////////////////////////////////////////////8=", + "ExLoRom": null, + "ExHiRom": null + }, + "Final Fantasy II (U) (V1.1)": { + "AddressMode": "LoRom", + "Size": 1048576, + "LoRom": "////////////////INCSa0ZJTkFMIEZBTlRBU1kgMiAgICAgICACCgMBwwHXiCh3/////////////wAC//8EAv///////////////wCA//8=", + "HiRom": "//////////////////////////////////////////////////////////////////////////////////////////////////////////8=", + "ExLoRom": null, + "ExHiRom": null + }, + "Final Fantasy IV (J)": { + "AddressMode": "LoRom", + "Size": 1048576, + "LoRom": "ILz+YK3jDyC8/mD/IPCUa0ZJTkFMIEZBTlRBU1kgNCAgICAgICACCgMAwwEzU8ys/////////////wAC//8EAv///////////////wCA//8=", + "HiRom": "//////////////////////////////////////////////////////////////////////////////////////////////////////////8=", + "ExLoRom": null, + "ExHiRom": null + }, + "Final Fantasy IV - 10th Anniversary Edition v3.21": { + "AddressMode": "LoRom", + "Size": 1572864, + "LoRom": "ILz+YK3jDyC8/mD/IPCUa0ZpbmFsIEZhbnRhc3kgNCAoSjJlKSACCgMAwwEzU8ys/////////////wAC//8EAiIA8iGABCJM8iFg/wCA//8=", + "HiRom": "FMTm4QHwAhEs3eLg3wFwAxHE4ist4AHwAyHdyd8AAP////////////9AAR0RlgDCIEiYGGlCAKho4iBg//9CABzfqP8V3eHfAAqFRgpg//8=", + "ExLoRom": null, + "ExHiRom": null + }, + "Final Fantasy IV - Ultima v5.12f no header": { + "AddressMode": "LoRom", + "Size": 1245184, + "LoRom": "////////////////INCSa0ZJTkFMIEZBTlRBU1kgMiAgICAgICACCgMBwwHXiCh3/////////////wAC//8EAv///////////////wCA//8=", + "HiRom": "jwZCAOrq6urq6urqrxZCAKrCIK8UQgAoevpIuQcq0AfiIJ1qIMIgaMllAJADqWQAelzV/wL///////////////////////////////////8=", + "ExLoRom": null, + "ExHiRom": null + }, + "Final Fantasy V (J)": { + "AddressMode": "HiRom", + "Size": 2097152, + "LoRom": "IiIiIgQEISMEIiIiIiIiBBEQFBMEIiIiIgQREwQEBAQRFBISEhIQJycnJycnKHNDQ3EmKAVzQ3SHh2AFFhhzQ3SCg4OEcHEmGFNUgq6IiK8=", + "HiRom": "dt8ybzSo0LhjyMDsS+gX+EZJTkFMIEZBTlRBU1kgNSAgICAgICECCwMAwwAN4PIfAAAAAAAAAAAAAODOAADkzgAAAAAAAAAAAAAAAMDOAAA=", + "ExLoRom": null, + "ExHiRom": null + }, + "Final Fantasy III (U) (V1.0) [!]": { + "AddressMode": "HiRom", + "Size": 3145728, + "LoRom": "gACcgCHK0PogqYggfoVgqc2Fy8IgpdAKqr8C5syFyaXQzwDmzJAFe+Ig5st74iCpAY1oBWCcZwWtuR4pQNAFrUUH0AScRQdgqWSNZwWpzoU=", + "HiRom": "QzNGNiAgAAAAAAAAAAAAAEZJTkFMIEZBTlRBU1kgMyAgICAgIDECDAMBMwDNoDJf/////////////xD///8U/////////////////wD///8=", + "ExLoRom": null, + "ExHiRom": null + }, + "Final Fantasy III (U) (V1.1) [!]": { + "AddressMode": "HiRom", + "Size": 3145728, + "LoRom": "gACcgCHK0PogqYggfoVgqc2Fy8IgpdAKqr8C5syFyaXQzwDmzJAFe+Ig5st74iCpAY1oBWCcZwWtuR4pQNAFrUUH0AScRQdgqWSNZwWpzoU=", + "HiRom": "QzNGNiAgAAAAAAAAAAAAAEZJTkFMIEZBTlRBU1kgMyAgICAgIDECDAMBMwGfdWCK/////////////xD///8U/////////////////wD///8=", + "ExLoRom": null, + "ExHiRom": null + }, + "Final Fantasy VI (J)": { + "AddressMode": "HiRom", + "Size": 3145728, + "LoRom": "gACcgCHK0Pog3ocgs4Rgqc2Fy8IgpdAKqr8C5syFyaXQzwDmzJAFe+Ig5st74iCpAY1oBWCcZwWtuR4pQNAFrUUH0AScRQdgqWSNZwWp5oU=", + "HiRom": "sLGys7S1tre4ubq7vL2+v0ZJTkFMIEZBTlRBU1kgNiAgICAgIDECDAMAwwCNXnKh4OHi4+Tl5ufo6RD/7O0U//Dx8vP09fb3+Pn6+wD//v8=", + "ExLoRom": null, + "ExHiRom": null + }, + "Firepower 2000 (U)": { + "AddressMode": "LoRom", + "Size": 1048576, + "LoRom": "/////////////////////0ZJUkVQT1dFUiAyMDAwICAgICAgIDAACgABuwDf4CAf/////wAAAAAAAEeBAAAugQAAAAAAAAAAAAAAAGyY//8=", + "HiRom": "//////////////////////////////////////////////////////////////////////////////////////////////////////////8=", + "ExLoRom": null, + "ExHiRom": null + }, + "Super SWIV (E)": { + "AddressMode": "LoRom", + "Size": 1048576, + "LoRom": "/////////////////////1NVUEVSIFNXSVYgICAgICAgICAgIDAACgACdQDgQh+9/////wAAAAAAAEeBAAAugQAAAAAAAAAAAAAAAGuY//8=", + "HiRom": "//////////////////////////////////////////////////////////////////////////////////////////////////////////8=", + "ExLoRom": null, + "ExHiRom": null + }, + "Super SWIV (J) (32469)": { + "AddressMode": "LoRom", + "Size": 1048576, + "LoRom": "/////////////////////1NVUEVSIFNXSVYgICAgICAgICAgIDAACgAACwAqgdV+/////wAAAAAAAEeBAAAugQAAAAAAAAAAAAAAAGuY//8=", + "HiRom": "//////////////////////////////////////////////////////////////////////////////////////////////////////////8=", + "ExLoRom": null, + "ExHiRom": null + }, + "Super SWIV (J) (62746) [b1]": { + "AddressMode": "LoRom", + "Size": 1048576, + "LoRom": "/////////////////////1NVUEVSIFNXSVYgICAgICAgICAgICAACgAACwDlChr1/////wAAAAAAAEeBAAAugQAAAAAAAAAAAAAAAGuY//8=", + "HiRom": "//////////////////////////////////////////////////////////////////////////////////////////////////////////8=", + "ExLoRom": null, + "ExHiRom": null + }, + "Super SWIV (J) (62746) [f1]": { + "AddressMode": "LoRom", + "Size": 1048576, + "LoRom": "/////////////////////0EuSS4gUFJFU0VOVFMgICAgICAgICAACgAACwDlChr1/////wAAAAAAAEeBAAAugQAAAAAAAAAAAAAAAGuY//8=", + "HiRom": "//////////////////////////////////////////////////////////////////////////////////////////////////////////8=", + "ExLoRom": null, + "ExHiRom": null + }, + "Super SWIV (J) (62746) [hI]": { + "AddressMode": "LoRom", + "Size": 1048576, + "LoRom": "/////////////////////1NVUEVSIFNXSVYgICAgICAgICAgICAACgAACwDlChr1/////wAAAAAAAEeBAAAugQAAAAAAAAAAAAAAAAD///8=", + "HiRom": "//////////////////////////////////////////////////////////////////////////////////////////////////////////8=", + "ExLoRom": null, + "ExHiRom": null + }, + "Super SWIV (J) (62746)": { + "AddressMode": "LoRom", + "Size": 1048576, + "LoRom": "/////////////////////1NVUEVSIFNXSVYgICAgICAgICAgICAACgAACwDlChr1/////wAAAAAAAEeBAAAugQAAAAAAAAAAAAAAAGuY//8=", + "HiRom": "//////////////////////////////////////////////////////////////////////////////////////////////////////////8=", + "ExLoRom": null, + "ExHiRom": null + }, + "Gradius III (U) [!]": { + "AddressMode": "LoRom", + "Size": 524288, + "LoRom": "/////////////////////2dyYWRpdXMgMyAgICAgICAgICAgICAACQABpABd2KIn/////1OFU4VThSOCAIBThf////9ThVOFU4UjggCAU4U=", + "HiRom": "//////////////////////////////////////////////////////////////////////////////////////////////////////////8=", + "ExLoRom": null, + "ExHiRom": null + }, + "Hagane (Beta)": { + "AddressMode": "HiRom", + "Size": 2097152, + "LoRom": "r77vquuvr7rqr76vr767rlNpbW9uIEplZmZlcnkurv7urrvr/+r6vqr+7u++u6/qu+6q6qqq7/67++qr6+v/7u767/u+6vu7/r7++6rqvro=", + "HiRom": "r+uqv6+7v66r7++u+r+v7khBR0FORS9DcmVhbSEgICAgICAgIDEACwAAGAD//wAAr677v66Hroeuh5QAAICQAO6+/u+uhwCArocAgACAAIA=", + "ExLoRom": null, + "ExHiRom": null + }, + "Hagane (E) [o1]": { + "AddressMode": "HiRom", + "Size": 2097152, + "LoRom": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", + "HiRom": "MThBSEdQAAAAAAAAAAAAAEhBR0FORSBFQyAgICAgICAgICAgIDEACwACMwAegOF/AAAAAMyHzIfMh5QAAICQAAAAAADMhwCAzIcAgACAAIA=", + "ExLoRom": null, + "ExHiRom": null + }, + "Hagane (E)": { + "AddressMode": "HiRom", + "Size": 2097152, + "LoRom": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", + "HiRom": "MThBSEdQAAAAAAAAAAAAAEhBR0FORSBFQyAgICAgICAgICAgIDEACwACMwAegOF/AAAAAMyHzIfMh5QAAICQAAAAAADMhwCAzIcAgACAAIA=", + "ExLoRom": null, + "ExHiRom": null + }, + "Hagane (J) [b1]": { + "AddressMode": "HiRom", + "Size": 2097152, + "LoRom": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", + "HiRom": "MThBSEdKAAAAAAAAAAAAAEhBR0FORSAgICAgICAgICAgICAgIDEACwAAMwDNbTKSAAAAACGIIYghiJQAAICQAAAAAAAhiACAIYgAgACAAIA=", + "ExLoRom": null, + "ExHiRom": null + }, + "Hagane (J) [t1]": { + "AddressMode": "HiRom", + "Size": 2097152, + "LoRom": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", + "HiRom": "MThBSEdKAAAAAAAAAAAAAEhBR0FORSAgICAgICAgICAgICAgIDEACwAAMwDNbTKSAAAAACGIIYghiJQAAICQAAAAAAAhiACAIYgAgACAAIA=", + "ExLoRom": null, + "ExHiRom": null + }, + "Hagane (J)": { + "AddressMode": "HiRom", + "Size": 2097152, + "LoRom": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", + "HiRom": "MThBSEdKAAAAAAAAAAAAAEhBR0FORSAgICAgICAgICAgICAgIDEACwAAMwDNbTKSAAAAACGIIYghiJQAAICQAAAAAAAhiACAIYgAgACAAIA=", + "ExLoRom": null, + "ExHiRom": null + }, + "Hagane (U) [t1]": { + "AddressMode": "HiRom", + "Size": 2097152, + "LoRom": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", + "HiRom": "MThBSEdFAAAAAAAAAAAAAEhBR0FORSBVU0EgICAgICAgICAgIDEACwABMwDpfxaAAAAAAMyHzIfMh5QAAICQAFwA0N/MhwCAzIcAgPD/AIA=", + "ExLoRom": null, + "ExHiRom": null + }, + "Hagane (U)": { + "AddressMode": "HiRom", + "Size": 2097152, + "LoRom": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", + "HiRom": "MThBSEdFAAAAAAAAAAAAAEhBR0FORSBVU0EgICAgICAgICAgIDEACwABMwDpfxaAAAAAAMyHzIfMh5QAAICQAAAAAADMhwCAzIcAgACAAIA=", + "ExLoRom": null, + "ExHiRom": null + }, + "Killer Instinct (E) [!]": { + "AddressMode": "HiRom", + "Size": 4194304, + "LoRom": "//////////////////////////////////////////////////////////////////////////////////////////////////////////8=", + "HiRom": "MDFBS0xQAAAAAAAAAAAAAEtJTExFUiBJTlNUSU5DVCAgICAgIDEADAACMwD1egqFUkFSRd7yA1De8gDyUoTe8lJBUkXe8t7y3vIA+FKEAFA=", + "ExLoRom": null, + "ExHiRom": null + }, + "Killer Instinct (U) (V1.0)": { + "AddressMode": "HiRom", + "Size": 4194304, + "LoRom": "//////////////////////////////////////////////////////////////////////////////////////////////////////////8=", + "HiRom": "MDFBS0xFAAAAAAAAAAAAAEtJTExFUiBJTlNUSU5DVCAgICAgIDEADAABMwA/usBFUkFSRQrzA1AK8yzyUoQK81JBUkUK8wrzCvMA+FKEAFA=", + "ExLoRom": null, + "ExHiRom": null + }, + "Killer Instinct (U) (V1.1) [!]": { + "AddressMode": "HiRom", + "Size": 4194304, + "LoRom": "//////////////////////////////////////////////////////////////////////////////////////////////////////////8=", + "HiRom": "MDFBS0xFAAAAAAAAAAAAAEtJTExFUiBJTlNUSU5DVCAgICAgIDEADAABMwGFinp1UkFSRcLyA1DpKuTxUVXC8lJBUkXC8lFUB90A+FKEAFA=", + "ExLoRom": null, + "ExHiRom": null + }, + "King of Dragons, The (J) [f1]": { + "AddressMode": "HiRom", + "Size": 2097152, + "LoRom": "Ii1+qqkBAJ0AAKACALcCGG3NHZ0PAKAGALcCGG3RHZ0XAKAEALcCnRMAoAgAtwIp/wCdBACgDAC3Ain/AJ00AKAOALcCnUYAGKUCaRAAhQI=", + "HiRom": "/////////////////////0tJTkcgT0YgRFJBR09OUyAgICAgIDEACwAACACK63UU/////6D/oP+g/6H/oP+l//////+g/6D/oP+g/6n/oP8=", + "ExLoRom": null, + "ExHiRom": null + }, + "King of Dragons, The (J) [t1]": { + "AddressMode": "HiRom", + "Size": 2097152, + "LoRom": "Ii1+qqkBAJ0AAKACALcCGG3NHZ0PAKAGALcCGG3RHZ0XAKAEALcCnRMAoAgAtwIp/wCdBACgDAC3Ain/AJ00AKAOALcCnUYAGKUCaRAAhQI=", + "HiRom": "/////////////////////0tJTkcgT0YgRFJBR09OUyAgICAgIDEACwAACACK63UU/////6D/oP+g/6H/oP+l//////+g/6D/oP+g/6n/oP8=", + "ExLoRom": null, + "ExHiRom": null + }, + "King of Dragons, The (J)": { + "AddressMode": "HiRom", + "Size": 2097152, + "LoRom": "Ii1+qqkBAJ0AAKACALcCGG3NHZ0PAKAGALcCGG3RHZ0XAKAEALcCnRMAoAgAtwIp/wCdBACgDAC3Ain/AJ00AKAOALcCnUYAGKUCaRAAhQI=", + "HiRom": "/////////////////////0tJTkcgT0YgRFJBR09OUyAgICAgIDEACwAACACK63UU/////6D/oP+g/6H/oP+l//////+g/6D/oP+g/6n/oP8=", + "ExLoRom": null, + "ExHiRom": null + }, + "King of Dragons, The (U) [h1]": { + "AddressMode": "HiRom", + "Size": 2097152, + "LoRom": "A0xzgPwDgZBJryItfqqpAQCdAACgAgC3AhhtzR2dDwCgBgC3Ahht0R2dFwCgBAC3Ap0TAKAIALcCKf8AnQQAoAwAtwIp/wCdNACgDgC3Ap0=", + "HiRom": "/////////////////////0tJTkcgT0YgRFJBR09OUyAgICAgIDEACwABCAABQf6+/////6D/oP+g/6H/oP+l//////+g/6D/oP+g/6n/oP8=", + "ExLoRom": null, + "ExHiRom": null + }, + "King of Dragons, The (U) [T+Ita]": { + "AddressMode": "HiRom", + "Size": 2097152, + "LoRom": "A0xzgPwDgZBJryItfqqpAQCdAACgAgC3AhhtzR2dDwCgBgC3Ahht0R2dFwCgBAC3Ap0TAKAIALcCKf8AnQQAoAwAtwIp/wCdNACgDgC3Ap0=", + "HiRom": "/////////////////////0tJTkcgT0YgRFJBR09OUyAgICAgIDEACwABCAABQf6+/////6D/oP+g/6H/oP+l//////+g/6D/oP+g/6n/oP8=", + "ExLoRom": null, + "ExHiRom": null + }, + "King of Dragons, The (U)": { + "AddressMode": "HiRom", + "Size": 2097152, + "LoRom": "A0xzgPwDgZBJryItfqqpAQCdAACgAgC3AhhtzR2dDwCgBgC3Ahht0R2dFwCgBAC3Ap0TAKAIALcCKf8AnQQAoAwAtwIp/wCdNACgDgC3Ap0=", + "HiRom": "/////////////////////0tJTkcgT0YgRFJBR09OUyAgICAgIDEACwABCAABQf6+/////6D/oP+g/6H/oP+l//////+g/6D/oP+g/6n/oP8=", + "ExLoRom": null, + "ExHiRom": null + }, + "Kirby Super Star (U) [!]": { + "AddressMode": "LoRom", + "Size": 4194304, + "LoRom": "MDFBS0ZFAAAAAAAAAAAAAEtJUkJZIFNVUEVSIERFTFVYRSAgICM1DAMBMwBVOarGAAAAAAAA/18AALGBAACihAAAAAAAAAAAAAAAAASAAAA=", + "HiRom": "6UET6cfy6Foa6QAA6Wgh6ar36G4C6UfR72/R75fR77/R79j/3VfQ73/Q76fQ74NAAqvczSBa//////////////////////////////////8=", + "ExLoRom": null, + "ExHiRom": null + }, + "Kirby's Avalanche (U) [!]": { + "AddressMode": "LoRom", + "Size": 1048576, + "LoRom": "MDFQUSAgAAAAAAAAAAAAAEtpcmJ5J3MgQXZhbGFuY2hlICAgIDAACgABMwDYECfvAAAAAAAAAAAAALSjAACfpAAAAAAAAAAAAABUswCAVLM=", + "HiRom": "OACNOgCNPACNPgAp/wDiIGuuAACpAJ8AIn7o4AAC0PapAY1vEmv///////////////////////////////////////////////////////8=", + "ExLoRom": null, + "ExHiRom": null + }, + "Kirby's Ghost Trap (E)": { + "AddressMode": "LoRom", + "Size": 1048576, + "LoRom": "MDFQUSAgAAAAAAAAAAAAAEtpcmJ5J3MgR2hvc3QgVHJhcCAgIDAACgACMwCfvmBBAAAAAAAAAAAAALSjAACfpAAAAAAAAAAAAABUswCAVLM=", + "HiRom": "qQGNbxJr//////////////////////////////////////////////////////////////////////////////////////////////////8=", + "ExLoRom": null, + "ExHiRom": null + }, + "Hoshi no Kirby 3 (J)": { + "AddressMode": "LoRom", + "Size": 4194304, + "LoRom": "MDFBRkpKAAAAAAAAAAAAAEhPU0hJIE5PIEtJUkJZIDMgICAgICM1DAUAMwDM3TMiAAAAAAAA/18AAPaCAABjgwAAAAAAAAAAAAAAAACAAAA=", + "HiRom": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", + "ExLoRom": null, + "ExHiRom": null + }, + "Kirby's Dream Land 3 (U)": { + "AddressMode": "LoRom", + "Size": 4194304, + "LoRom": "MDFBRkpFAAAAAAAAAAAAAEtJUkJZJ1MgRFJFQU0gTEFORCAzICM1DAUBMwCD23wkAAAAAAAA/18AAPaCAABjgwAAAAAAAAAAAAAAAACAAAA=", + "HiRom": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", + "ExLoRom": null, + "ExHiRom": null + }, + "Kaizoalttp": { + "AddressMode": "LoRom", + "Size": 2097152, + "LoRom": "AY0kAeIwa////////////1pFTERBTk9ERU5TRVRTVSAgICAgICACCwMAAQA3MsjN/////yyC//8sgsmAAIDYgv////8sgiyCLIIsggCA2II=", + "HiRom": "pI+qwX5rreAC0AavVvN+0BevV/N+8AOc4AKpDIVLqSqmG/ACqRSFEWupDIVLqSqmG/ACqRSFEa9t83446QiPbfN+yaiQBqkAj23zfmv///8=", + "ExLoRom": null, + "ExHiRom": null + }, + "Legend of Zelda, The - A Link to the Past (E) [!]": { + "AddressMode": "LoRom", + "Size": 1048576, + "LoRom": "AY0kAeIwa////////////1RIRSBMRUdFTkQgT0YgWkVMREEgICACCgMCAQBGirl1/////yyC//8sgsmAAIDYgv////8sgiyCLIIsggCA2II=", + "HiRom": "pI+qwX5rreAC0AavVvN+0BevV/N+8AOc4AKpDIVLqSqmG/ACqRSFEWupDIVLqSqmG/ACqRSFEa9t83446QiPbfN+yaiQBqkAj23zfmv///8=", + "ExLoRom": null, + "ExHiRom": null + }, + "Legend of Zelda, The - A Link to the Past (F)": { + "AddressMode": "LoRom", + "Size": 1048576, + "LoRom": "/////////////////////0xBIExFR0VOREUgREUgWkVMREEgICACCgMGAQBRmq5l/////zGC//8xgsmAAIDdgv////8xgjGCMYIxggCA3YI=", + "HiRom": "fsjI6OjgENDV4iCiAKBApYrJQ/AEyUXQD6/D8n4pINAYpRopDAoKqMIguc7+n9DFfsjI6OjgENDv4iCr5hVr//////////////////////8=", + "ExLoRom": null, + "ExHiRom": null + }, + "Legend of Zelda, The - A Link to the Past (FC)": { + "AddressMode": "LoRom", + "Size": 1048576, + "LoRom": "/////////////////////0xBIExFR0VOREUgREUgWkVMREEgICACCgMBAQBDR7y4/////zGC//8xgsmAAIDdgv////8xgjGCMYIxggCA3YI=", + "HiRom": "pI+qwX5rreAC0AavVvN+0BevV/N+8AOc4AKpDIVLqSqmG/ACqRSFEWupDIVLqSqmG/ACqRSFEa9t83446QiPbfN+yaiQBqkAj23zfmv///8=", + "ExLoRom": null, + "ExHiRom": null + }, + "Legend of Zelda, The - A Link to the Past (G) [!]": { + "AddressMode": "LoRom", + "Size": 1048576, + "LoRom": "/////////////////////1RIRSBMRUdFTkQgT0YgWkVMREEgICACCgMJAQBJr7ZQ/////zGC//8xgsmAAIDdgv////8xgjGCMYIxggCA3YI=", + "HiRom": "fsjI6OjgENDV4iCiAKBApYrJQ/AEyUXQD6/D8n4pINAYpRopDAoKqMIguc7+n9DFfsjI6OjgENDv4iCr5hVr//////////////////////8=", + "ExLoRom": null, + "ExHiRom": null + }, + "Legend of Zelda, The - A Link to the Past (U) [!]-rand-pal": { + "AddressMode": "LoRom", + "Size": 1048576, + "LoRom": "AY0kAeIwa////////////1RIRSBMRUdFTkQgT0YgWkVMREEgICACCgMBAQDyUA2v/////yyC//8sgsmAAIDYgv////8sgiyCLIIsggCA2II=", + "HiRom": "pI+qwX5rreAC0AavVvN+0BevV/N+8AOc4AKpDIVLqSqmG/ACqRSFEWupDIVLqSqmG/ACqRSFEa9t83446QiPbfN+yaiQBqkAj23zfmv///8=", + "ExLoRom": null, + "ExHiRom": null + }, + "Legend of Zelda, The - A Link to the Past (U) [!]": { + "AddressMode": "LoRom", + "Size": 1048576, + "LoRom": "AY0kAeIwa////////////1RIRSBMRUdFTkQgT0YgWkVMREEgICACCgMBAQDyUA2v/////yyC//8sgsmAAIDYgv////8sgiyCLIIsggCA2II=", + "HiRom": "pI+qwX5rreAC0AavVvN+0BevV/N+8AOc4AKpDIVLqSqmG/ACqRSFEWupDIVLqSqmG/ACqRSFEa9t83446QiPbfN+yaiQBqkAj23zfmv///8=", + "ExLoRom": null, + "ExHiRom": null + }, + "Zelda no Densetsu - Kamigami no Triforce (J) (V1.0) [b1]": { + "AddressMode": "LoRom", + "Size": 1048576, + "LoRom": "AY0kAeIwa////////////1pFTERBTk9ERU5TRVRTVSAgICAgICACCgMAAQA3MsjN/////yyC//8sgsmAAIDYgv////8sgiyCLIIsggCA2II=", + "HiRom": "pI+qwX5rreAC0AavVvN+0BevV/N+8AOc4AKpDIVLqSqmG/ACqRSFEWupDIVLqSqmG/ACqRSFEa9t83446QiPbfN+yaiQBqkAj23zfmv///8=", + "ExLoRom": null, + "ExHiRom": null + }, + "Zelda no Densetsu - Kamigami no Triforce (J) (V1.0)": { + "AddressMode": "LoRom", + "Size": 1048576, + "LoRom": "AY0kAeIwa////////////1pFTERBTk9ERU5TRVRTVSAgICAgICACCgMAAQA3MsjN/////yyC//8sgsmAAIDYgv////8sgiyCLIIsggCA2II=", + "HiRom": "pI+qwX5rreAC0AavVvN+0BevV/N+8AOc4AKpDIVLqSqmG/ACqRSFEWupDIVLqSqmG/ACqRSFEa9t83446QiPbfN+yaiQBqkAj23zfmv///8=", + "ExLoRom": null, + "ExHiRom": null + }, + "Zelda no Densetsu - Kamigami no Triforce (J) (V1.1)": { + "AddressMode": "LoRom", + "Size": 1048576, + "LoRom": "AY0kAeIwa////////////1pFTERBTk9ERU5TRVRTVSAgICAgICACCgMAAQFv8pAN/////yyC//8sgsmAAIDYgv////8sgiyCLIIsggCA2II=", + "HiRom": "pI+qwX5rreAC0AavVvN+0BevV/N+8AOc4AKpDIVLqSqmG/ACqRSFEWupDIVLqSqmG/ACqRSFEa9t83446QiPbfN+yaiQBqkAj23zfmv///8=", + "ExLoRom": null, + "ExHiRom": null + }, + "Zelda no Densetsu - Kamigami no Triforce (J) (V1.2)": { + "AddressMode": "LoRom", + "Size": 1048576, + "LoRom": "AY0kAeIwa////////////1pFTERBTk9ERU5TRVRTVSAgICAgICACCgMAAQIc9OML/////yyC//8sgsmAAIDYgv////8sgiyCLIIsggCA2II=", + "HiRom": "pI+qwX5rreAC0AavVvN+0BevV/N+8AOc4AKpDIVLqSqmG/ACqRSFEWupDIVLqSqmG/ACqRSFEa9t83446QiPbfN+yaiQBqkAj23zfmv///8=", + "ExLoRom": null, + "ExHiRom": null + }, + "Zelda Parallel Worlds": { + "AddressMode": "LoRom", + "Size": 1572864, + "LoRom": "AY0kAeIwa////////////0xPWjogUEFSQUxMRUwgV09STERTICACDAMBAQA3pcha/////yyC//8sgsmAAIDYgv////8sgiyCLIIsggCA2II=", + "HiRom": "pI+qwX5rreAC0AavVvN+0BevV/N+8AOc4AKpDIVLqSqmG/ACqRSFEWupDIVLqSqmG/ACqRSFEa9t83446QiPbfN+yaiQBqkAj23zfmv///8=", + "ExLoRom": null, + "ExHiRom": null + }, + "Magical Quest Starring Mickey Mouse, The (U) [!]": { + "AddressMode": "LoRom", + "Size": 1048576, + "LoRom": "/L/0n/dxor3C/t//7/9//1RIRSBNQUdJQ0FMIFFVRVNUICAgICAACgABCAC1nkphXEm7AfGH7Yf9uwWB9/+ugn52+S9O0zNn2fX99wiA//8=", + "HiRom": "jQDQyFw4uwGv/+/99/fv//Hfu+7/r331mtPn23+e/+/+u/f9vr9/L5v3/zz/3//f7/r/Ou/75f///7z+//33///vfX6eft9e/+u/n+///v8=", + "ExLoRom": null, + "ExHiRom": null + }, + "Marko's Magic Football (E) [f1]": { + "AddressMode": "LoRom", + "Size": 2097152, + "LoRom": "NTFBTVJQAAAAAAAAAAAAAE1BUktPUyBNQUdJQyBGT09UQkFMTCAACwACMwDkthtJ/wAA//GJ7InricKJ/wCfif8AAP//AAD//wAA/wCAAP8=", + "HiRom": "/wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP8=", + "ExLoRom": null, + "ExHiRom": null + }, + "Marko's Magic Football (E)": { + "AddressMode": "LoRom", + "Size": 2097152, + "LoRom": "NTFBTVJQAAAAAAAAAAAAAE1BUktPUyBNQUdJQyBGT09UQkFMTDAACwACMwCEtntJ/wAA//GJ7InricKJ/wCfif8AAP//AAD//wAA/wCAAP8=", + "HiRom": "/wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP8=", + "ExLoRom": null, + "ExHiRom": null + }, + "Mega Man X (E)": { + "AddressMode": "LoRom", + "Size": 1572864, + "LoRom": "AAD//////////////////01FR0FNQU4gWCAgICAgICAgICAgIDAACwACAQAUsetO/////6z/rP+s/6T/rP+o//////+s/6z/rP+s/wCArP8=", + "HiRom": "//////////////////////////////////////////////////////////////////////////////////////////////////////////8=", + "ExLoRom": null, + "ExHiRom": null + }, + "Mega Man X (U) (V1.0) [!]": { + "AddressMode": "LoRom", + "Size": 1572864, + "LoRom": "AAD//////////////////01FR0FNQU4gWCAgICAgICAgICAgIDAACwABCAAEqvtV/////6z/rP+s/6T/rP+o//////+s/6z/rP+s/wCArP8=", + "HiRom": "//////////////////////////////////////////////////////////////////////////////////////////////////////////8=", + "ExLoRom": null, + "ExHiRom": null + }, + "Mega Man X (U) (V1.1)": { + "AddressMode": "LoRom", + "Size": 1572864, + "LoRom": "AAD//////////////////01FR0FNQU4gWCAgICAgICAgICAgIDAACwABCAE9s8JM/////6z/rP+s/6T/rP+o//////+s/6z/rP+s/wCArP8=", + "HiRom": "//////////////////////////////////////////////////////////////////////////////////////////////////////////8=", + "ExLoRom": null, + "ExHiRom": null + }, + "Rockman X (J) (V1.0) [!]": { + "AddressMode": "LoRom", + "Size": 1572864, + "LoRom": "AAD//////////////////1JPQ0tNQU4gWCAgICAgICAgICAgIDAACwAACACWmmll/////6z/rP+s/6T/rP+o//////+s/6z/rP+s/wCArP8=", + "HiRom": "//////////////////////////////////////////////////////////////////////////////////////////////////////////8=", + "ExLoRom": null, + "ExHiRom": null + }, + "Rockman X (J) (V1.1)": { + "AddressMode": "LoRom", + "Size": 1572864, + "LoRom": "AAD//////////////////1JPQ0tNQU4gWCAgICAgICAgICAgIDAACwAACAEotNdL/////6z/rP+s/6T/rP+o//////+s/6z/rP+s/wCArP8=", + "HiRom": "//////////////////////////////////////////////////////////////////////////////////////////////////////////8=", + "ExLoRom": null, + "ExHiRom": null + }, + "Ms. Pac-Man (U)": { + "AddressMode": "LoRom", + "Size": 262144, + "LoRom": "NURBTjhFAAAAAAAAAAAAAE1TUEFDTUFOICAgICAgICAgICAgIDAACAABMwCJR3a4/wD/AAAAAAAAAB+BAAAYgQAAAAAAAAAAAAAfgQCAGIE=", + "HiRom": "/wD/AP8A/wD/AP8A/wD/AP8A/wD/AP8A/wD/AP8A/wD/AP8A/wD/AP8A/wD/AP8A/wD/AP8A/wD/AP8A/wD/AP8A/wD/AP8A/wD/AP8A/wA=", + "ExLoRom": null, + "ExHiRom": null + }, + "Pilotwings (E) [!]": { + "AddressMode": "LoRom", + "Size": 524288, + "LoRom": "AAAAAAAAAAAAAAAAAAAAAFBJTE9UV0lOR1MgICAgICAgICAgICADCQACAQDzHAzjAAAAAAjyCPII8qrwAADo8QAAAAAI8gjyCPIAAADwAAA=", + "HiRom": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGmxG3CAA4Dc=", + "ExLoRom": null, + "ExHiRom": null + }, + "Pilotwings (J)": { + "AddressMode": "LoRom", + "Size": 524288, + "LoRom": "AAAAAAAAAAAAAAAAAAAAAFBJTE9UV0lOR1MgICAgICAgICAgICADCQAAAQAIM/fMAAAAAP7x/vH+8aDwAADe8QAAAAD+8f7x/vEAAADwAAA=", + "HiRom": "AEgIBYgAYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAECwTEgtfbgANR5IdCIWEjc=", + "ExLoRom": null, + "ExHiRom": null + }, + "Pilotwings (U) [!]": { + "AddressMode": "LoRom", + "Size": 524288, + "LoRom": "AAAAAAAAAAAAAAAAAAAAAFBJTE9UV0lOR1MgICAgICAgICAgICADCQABAQCczmMxAAAAAP7x/vH+8aDwAADe8QAAAAD+8f7x/vEAAADwAAA=", + "HiRom": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGmxG3CAA4Dc=", + "ExLoRom": null, + "ExHiRom": null + }, + "Pilotwings (U) [f1]": { + "AddressMode": "LoRom", + "Size": 524288, + "LoRom": "AAAAAAAAAAAAAAAAAAAAAFBJTE9UV0lOR1MgICAgICAgICAgICADCQABAQCczmMxAAAAAP7x/vH+8aDwAADe8QAAAAD+8f7x/vEAAADwAAA=", + "HiRom": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGmxG3CAA4Dc=", + "ExLoRom": null, + "ExHiRom": null + }, + "Power Lode Runner (J) (NP)": { + "AddressMode": "LoRom", + "Size": 1572864, + "LoRom": "MDFCUExKAAAAAAAAAAAAAFBPV0VSIExPREUgUlVOTkVSICAgIDACCwMAMwDwvQ9CAAAAAAAAAAAAAGKCBYBDgwAAAAAAAAAAAABiggWAQ4M=", + "HiRom": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", + "ExLoRom": null, + "ExHiRom": null + }, + "Run Saber (Beta) [h1]": { + "AddressMode": "LoRom", + "Size": 1048576, + "LoRom": "UkNSID0gOTMvMDEvMTkgAFJVTlNBQkVSICAgIFwAgCAgICAgICAACgAB6wD//wAAeNwAAPD/8P/w/+D/If9AgEAAAADw//D/8P/g/yH/QIA=", + "HiRom": "7q6q7qqurquq7vqqqu7/q7+uqrrqvquq6qrr7r+qr7rv/q/uuqrqrqu6rqqvrrrur+u6qu6q+qr/qrrv7666qu7qv+qu6uqvqq6uvuqu7qo=", + "ExLoRom": null, + "ExHiRom": null + }, + "Run Saber (Beta)": { + "AddressMode": "LoRom", + "Size": 1310720, + "LoRom": "UkNSID0gOTMvMDEvMTkgAFJVTlNBQkVSICAgICAgICAgICAgIDAACgAB6wD//wAAeNwAAPD/8P/w/+D/If9AgEAAAADw//D/8P/g/8z/QIA=", + "HiRom": "7q6q7qqurquq7vqqqu7/q7+uqrrqvquq6qrr7r+qr7rv/q/uuqrqrqu6rqqvrrrur+u6qu6q+qr/qrrv7666qu7qv+qu6uqvqq6uvuqu7qo=", + "ExLoRom": null, + "ExHiRom": null + }, + "Run Saber (E) [!]": { + "AddressMode": "LoRom", + "Size": 1048576, + "LoRom": "UkNSID0gOTMvMDEvMTkgAFJVTlNBQkVSICAgICAgICAgICAgIDAACgAC6wBkyZs2eNwAAPD/8P/w/+D/If9AgEAAAADw//D/8P/g/yH/QIA=", + "HiRom": "QCpX7ewIQij/DO0IQihYGe0IQihYKe0IQihYOe0IQihYVO2O6ftuy///zt37/7/t2+Oz1J/btv13/zWm///13wGXfq/rfq/x//m//////74=", + "ExLoRom": null, + "ExHiRom": null + }, + "Run Saber (U) [!]": { + "AddressMode": "LoRom", + "Size": 1048576, + "LoRom": "UkNSID0gOTMvMDEvMTkgAFJVTlNBQkVSICAgICAgICAgICAgIDAACgAB6wBPtrBJeNwAAPD/8P/w/+D/If9AgEAAAADw//D/8P/g/yH/QIA=", + "HiRom": "QCpX7ewIQij/DO0IQihYGe0IQihYKe0IQihYOe0IQihYVO2O6ftuy///zt37/7/t2+Oz1J/btv13/zWm///13wGXfq/rfq/x//m//////74=", + "ExLoRom": null, + "ExHiRom": null + }, + "Run Saber (U) [t1]": { + "AddressMode": "LoRom", + "Size": 1081344, + "LoRom": "UkNSID0gOTMvMDEvMTkgAFJVTlNBQkVSICAgIFwAgCAgICAgICAACgAB6wD//wAAeNwAAPD/8P/w/+D/If9AgEAAAADw//D/8P/g/8z/QIA=", + "HiRom": "7q6q7qqurquq7vqqqu7/q7+uqrrqvquq6qrr7r+qr7rv/q/uuqrqrqu6rqqvrrrur+u6qu6q+qr/qrrv7666qu7qv+qu6uqvqq6uvuqu7qo=", + "ExLoRom": null, + "ExHiRom": null + }, + "Run Saber (U) [t2]": { + "AddressMode": "LoRom", + "Size": 1179648, + "LoRom": "UkNSID0gOTMvMDEvMTkgAFJVTlNBQkVSICAgIFwAgCAgICAgICAACgAB6wD//wAAeNwAAPD/8P/w/+D/If9AgEAAAADw//D/8P/g/8z/QIA=", + "HiRom": "7q6q7qqurquq7vqqqu7/q7+uqrrqvquq6qrr7r+qr7rv/q/uuqrqrqu6rqqvrrrur+u6qu6q+qr/qrrv7666qu7qv+qu6uqvqq6uvuqu7qo=", + "ExLoRom": null, + "ExHiRom": null + }, + "Run Saber (U) [t3]": { + "AddressMode": "LoRom", + "Size": 1081344, + "LoRom": "UkNSID0gOTMvMDEvMTkgAFJVTlNBQkVSICAgIFwAgCAgICAgICAACgAB6wD//wAAeNwAAPD/8P/w/+D/If9AgEAAAADw//D/8P/g/8z/QIA=", + "HiRom": "7q6q7qqurquq7vqqqu7/q7+uqrrqvquq6qrr7r+qr7rv/q/uuqrqrqu6rqqvrrrur+u6qu6q+qr/qrrv7666qu7qv+qu6uqvqq6uvuqu7qo=", + "ExLoRom": null, + "ExHiRom": null + }, + "Secret of Mana (E) (V1.0)": { + "AddressMode": "HiRom", + "Size": 2097152, + "LoRom": "HfAG2iKVAMD6qdmNAx2OAR0ikgDAIsHV0MIwIsHV0CCTf4nAz9DyIJN/icDP8PjiIKmALAQY0AmpBAwCGCLB1dBgBBjQCakEDAIYIrTV0GA=", + "HiRom": "AQABAAEAAQEAAQABAAEBAFNlY3JldCBvZiBNQU5BICAgICAgICECCwMCAQBRya42/////////////wABBIAEAQAAAAAAAAAAAAAAAASA//8=", + "ExLoRom": null, + "ExHiRom": null + }, + "Secret of Mana (E) (V1.1) [!]": { + "AddressMode": "HiRom", + "Size": 2097152, + "LoRom": "HfAG2iKVAMD6qdmNAx2OAR0ikgDAIsHV0MIwIsHV0CCTf4nAz9DyIJN/icDP8PjiIKmALAQY0AmpBAwCGCLB1dBgBBjQCakEDAIYIrTV0GA=", + "HiRom": "AQABAAEAAQEAAQABAAEBAFNlY3JldCBvZiBNQU5BICAgICAgICECCwMCAQGti1J0/////////////wABBIAEAQAAAAAAAAAAAAAAAASA//8=", + "ExLoRom": null, + "ExHiRom": null + }, + "Secret of Mana (F)": { + "AddressMode": "HiRom", + "Size": 2097152, + "LoRom": "HfAG2iKVAMD6qdmNAx2OAR0ikgDAIsHV0MIwIsHV0CCTf4nAz9DyIJN/icDP8PjiIKmALAQY0AmpBAwCGCLB1dBgBBjQCakEDAIYIrTV0GA=", + "HiRom": "AQABAAEAAQEAAQABAAEBAFNlY3JldCBvZiBNQU5BICAgICAgICECCwMGAQCwik91/////////////wABBIAEAQAAAAAAAAAAAAAAAASA//8=", + "ExLoRom": null, + "ExHiRom": null + }, + "Secret of Mana (G)": { + "AddressMode": "HiRom", + "Size": 2097152, + "LoRom": "HfAG2iKVAMD6qdmNAx2OAR0ikgDAIsHV0MIwIsHV0CCTf4nAz9DyIJN/icDP8PjiIKmALAQY0AmpBAwCGCLB1dBgBBjQCakEDAIYIrTV0GA=", + "HiRom": "AQABAAEAAQEAAQABAAEBAFNlY3JldCBvZiBNQU5BICAgICAgICECCwMJAQAeSuG1/////////////wABBIAEAQAAAAAAAAAAAAAAAASA//8=", + "ExLoRom": null, + "ExHiRom": null + }, + "Secret of Mana (U) [!]": { + "AddressMode": "HiRom", + "Size": 2097152, + "LoRom": "HfAG2iKVAMD6qdmNAx2OAR0ikgDAIsHV0MIwIsHV0CCTf4nAz9DyIJN/icDP8PjiIKmALAQY0AmpBAwCGCLB1dBgBBjQCakEDAIYIrTV0GA=", + "HiRom": "AQABAAEAAQEAAQABAAEBAFNlY3JldCBvZiBNQU5BICAgICAgICECCwMBwwADrvxR/////////////wABBIAEAQAAAAAAAAAAAAAAAASA//8=", + "ExLoRom": null, + "ExHiRom": null + }, + "Seiken Densetsu 2 (J)": { + "AddressMode": "HiRom", + "Size": 2097152, + "LoRom": "HfAG2iKVAMD6qdmNAx2OAR0ikgDAIsHV0MIwIsHV0CCTf4nAz9DyIJN/icDP8PjiIKmALAQY0AmpBAwCGCLB1dBgBBjQCakEDAIYIrTV0GA=", + "HiRom": "AQABAAEAAQEAAQABAAEBAFNlaWtlbkRlbnNldHN1IDIgICAgICECCwMAwwCDJnzZ/////////////wABBIAEAQAAAAAAAAAAAAAAAASA//8=", + "ExLoRom": null, + "ExHiRom": null + }, + "Shin Megami Tensei (J) (V1.0)": { + "AddressMode": "LoRom", + "Size": 1572864, + "LoRom": "AAS7tQAEAgQABLu1AAQCBERJR0lUQUwgREVWSUwgU1RPUlkgICACCwMA6wCLUnStAAS7tQAEt7UABPiCAIBOhgAEt7UABLe1AAT4ggCAToY=", + "HiRom": "oAauFQedWhAYa6wVB75aEL0CECkAwNAIvQQQjT4KGGA4YKksACLFgAFgBsIwKf8ArogG8AcYaQYAytD5Cqi5oAauFQedWhAYa6wVB75aEL0=", + "ExLoRom": null, + "ExHiRom": null + }, + "Shin Megami Tensei (J) (V1.1)": { + "AddressMode": "LoRom", + "Size": 1572864, + "LoRom": "NwQAADcEAAA3BAAANwQAAERJR0lUQUwgREVWSUwgU1RPUlkgICACCwMA6wGwQE+/NwQAADcEAAA3BPiCAIBOhjcEAAA3BAAANwT4ggCAToY=", + "HiRom": "//+dWhA4a43hBsIwKf8ArogG8AcYaQYAytD5Cqi5oAauFQedWhAYa6wVB75aEL0CECkAwNAIvQQQjT4KGGA4YKksACLFgAFg7rv/7q7+xSA=", + "ExLoRom": null, + "ExHiRom": null + }, + "Space Megaforce (U) [!]": { + "AddressMode": "LoRom", + "Size": 1048576, + "LoRom": "/6MRiYL/77oLY0ujs5PrLVNQQUNFIE1FR0FGT1JDRSAgICAgICAACgABrQDuLhHR9fkCwQAAAAAAACy9AADKvQAADwEAAAAAAAAY4gCAGOI=", + "HiRom": "/XsT03B1HZe7eNV4+7/RV34/f9xj1y9renMNPYgef3NDimlJr8ESK8fndWDO1dV77menf/YbDs3Tx/m/e68NY3tW11pe6+vXdx0Ilcjh7ZM=", + "ExLoRom": null, + "ExHiRom": null + }, + "Space Megaforce (U) [T+Spa100%_Sayans]": { + "AddressMode": "LoRom", + "Size": 1048576, + "LoRom": "/6MRiYL/77oLY0ujs5PrLVNQQUNFIE1FR0FGT1JDRSAgICAgICAACgABrQDuLhHR9fkCwQAAAAAAACy9AADKvQAADwEAAAAAAAAY4gCAGOI=", + "HiRom": "/XsT03B1HZe7eNV4+7/RV34/f9xj1y9renMNPYgef3NDimlJr8ESK8fndWDO1dV77menf/YbDs3Tx/m/e68NY3tW11pe6+vXdx0Ilcjh7ZM=", + "ExLoRom": null, + "ExHiRom": null + }, + "Super Aleste (E) [h1C]": { + "AddressMode": "LoRom", + "Size": 1048576, + "LoRom": "/6MBiYL/r7oL40+ns5PpKVN1cGVyIEFsZXN0ZSAgICAgICAgICAACgACrQC96UIW9fmCwQAAAAAAADK9AADQvQAADwEAAAAAAABezwCAXs8=", + "HiRom": "/XtTU3F3XZe7eNV4e79RV3w//9xD1y9jenMNPcgef3NDimlJr8FSK8fndWDO1fV7/mO/f/ZbDs3bx3m/e68NY3tW1lpe6+vX5x0Il8jp7ZM=", + "ExLoRom": null, + "ExHiRom": null + }, + "Super Aleste (E)": { + "AddressMode": "LoRom", + "Size": 1048576, + "LoRom": "/6MBiYL/r7oL40+ns5PpKVNVUEVSIEFMRVNURSAgICAgICAgICAACgACrQDd6iIV9fmCwQAAAAAAADK9AADQvQAADwEAAAAAAABezwCAXs8=", + "HiRom": "/XtTU3F3XZe7eNV4e79RV3w//9xD1y9jenMNPcgef3NDimlJr8FSK8fndWDO1fV7/mO/f/ZbDs3bx3m/e68NY3tW1lpe6+vX5x0Il8jp7ZM=", + "ExLoRom": null, + "ExHiRom": null + }, + "Super Aleste (J) [t1]": { + "AddressMode": "HiRom", + "Size": 1048576, + "LoRom": "/6MRiYL/77oL60+js5PrKcIwSNpaiwviMKmZjVcBqZmNUgGpBo1vAMIwTB/C9/YI1fkCwQAAAAAAAMD/AACywgAADwEAAAAAAADx5gCA8eY=", + "HiRom": "/XsTU3B1HZe7edV4e/9RX35//9xj1y9je3MNPYgef3NDimlJr8FSO8fldWDu1fV7/mWnf7YbDs3bwXm/e68PY3pW11pe6+vX5x0Ilcjh7ZM=", + "ExLoRom": null, + "ExHiRom": null + }, + "Super Aleste (J) [t2]": { + "AddressMode": "LoRom", + "Size": 1048576, + "LoRom": "/6MRiYL/77oL60+js5PrKVNVUEVSIEFMRVNURSAgICAgICAgICAACgAArQAJ9/YI1fkCwQAAAAAAABjCAACywgAADwEAAAAAAADx5gCA8eY=", + "HiRom": "/XsTU3B1HZe7edV4e/9RX35//9xj1y9je3MNPYgef3NDimlJr8FSO8fldWDu1fV7/mWnf7YbDs3bwXm/e68PY3pW11pe6+vX5x0Ilcjh7ZM=", + "ExLoRom": null, + "ExHiRom": null + }, + "Super Aleste (J)": { + "AddressMode": "LoRom", + "Size": 1048576, + "LoRom": "/6MRiYL/77oL60+js5PrKVNVUEVSIEFMRVNURSAgICAgICAgICAACgAArQAJ9/YI1fkCwQAAAAAAABjCAACywgAADwEAAAAAAADx5gCA8eY=", + "HiRom": "/XsTU3B1HZe7edV4e/9RX35//9xj1y9je3MNPYgef3NDimlJr8FSO8fldWDu1fV7/mWnf7YbDs3bwXm/e68PY3pW11pe6+vX5x0Ilcjh7ZM=", + "ExLoRom": null, + "ExHiRom": null + }, + "Star Fox (J) [!]": { + "AddressMode": "LoRom", + "Size": 1048576, + "LoRom": "/////////////////////1NUQVIgRk9YICAgICAgICAgICAgICATCgAAAQDtVBKr/////5r/mv+a/wgBAAAMAf////+a/wAAmv+a/5b/mv8=", + "HiRom": "//////////////////////////////////////////////////////////////////////////////////////////////////////////8=", + "ExLoRom": null, + "ExHiRom": null + }, + "Star Fox (U) (V1.0) [!]": { + "AddressMode": "LoRom", + "Size": 1048576, + "LoRom": "/////////////////////1NUQVIgRk9YICAgICAgICAgICAgICATCgABAQBGCbn2/////5r/mv+a/wgBAAAMAf////+a/wAAmv+a/5b/mv8=", + "HiRom": "//////////////////////////////////////////////////////////////////////////////////////////////////////////8=", + "ExLoRom": null, + "ExHiRom": null + }, + "Star Fox (U) (V1.2) [!]": { + "AddressMode": "LoRom", + "Size": 1048576, + "LoRom": "/////////////////////1NUQVIgRk9YICAgICAgICAgICAgICATCgABAQLPBjD5/////57/nv+e/wgBAAAMAf////+e/wAAnv+e/5z/nv8=", + "HiRom": "//////////////////////////////////////////////////////////////////////////////////////////////////////////8=", + "ExLoRom": null, + "ExHiRom": null + }, + "StarWing (E) (V1.0) [!]": { + "AddressMode": "LoRom", + "Size": 1048576, + "LoRom": "/////////////////////1NUQVIgV0lORyAgICAgICAgICAgICATCgACAQAA//8A/////5r/mv+a/wgBAAAMAf////+a/wAAmv+a/5b/mv8=", + "HiRom": "//////////////////////////////////////////////////////////////////////////////////////////////////////////8=", + "ExLoRom": null, + "ExHiRom": null + }, + "StarWing (E) (V1.1) [!]": { + "AddressMode": "LoRom", + "Size": 1048576, + "LoRom": "/////////////////////1NUQVIgV0lORyAgICAgICAgICAgICATCgACAQFIELfv/////57/nv+e/wgBAAAMAf////+e/wAAnv+e/5z/nv8=", + "HiRom": "//////////////////////////////////////////////////////////////////////////////////////////////////////////8=", + "ExLoRom": null, + "ExHiRom": null + }, + "StarWing (G) [!]": { + "AddressMode": "LoRom", + "Size": 1048576, + "LoRom": "/////////////////////1NUQVIgV0lORyAgICAgICAgICAgICATCgAJAQA8R8O4/////57/nv+e/wgBAAAMAf////+e/wAAnv+e/5z/nv8=", + "HiRom": "//////////////////////////////////////////////////////////////////////////////////////////////////////////8=", + "ExLoRom": null, + "ExHiRom": null + }, + "StarWing Offizieller Wettbewerb (G) [!]": { + "AddressMode": "LoRom", + "Size": 1048576, + "LoRom": "/////////////////////1NUQVIgV0lORyAgICAgICAgICAgICATCgAJAQAqlNVr/////3b8dvx2/AgBAAAMAf////92/AAAdvx2/HT8dvw=", + "HiRom": "//////////////////////////////////////////////////////////////////////////////////////////////////////////8=", + "ExLoRom": null, + "ExHiRom": null + }, + "StarWing Super Weekend Competition (E) [!]": { + "AddressMode": "LoRom", + "Size": 1048576, + "LoRom": "/////////////////////1NUQVIgV0lORyAgICAgICAgICAgICATCgACAQBQgq99/////3b8dvx2/AgBAAAMAf////92/AAAdvx2/HT8dvw=", + "HiRom": "//////////////////////////////////////////////////////////////////////////////////////////////////////////8=", + "ExLoRom": null, + "ExHiRom": null + }, + "Stunt Race FX (E) [!]": { + "AddressMode": "LoRom", + "Size": 1048576, + "LoRom": "MDFDUSAgAAAAAAAAAAYAAFN0dW50IFJhY2UgRlggICAgICAgICAVCgACMwAjANz//////5D+kP6Q/ggBAAAMAf////+Q/gAAkP6Q/oj+kP4=", + "HiRom": "//////////////////////////////////////////////////////////////////////////////////////////////////////////8=", + "ExLoRom": null, + "ExHiRom": null + }, + "Stunt Race FX (U) [!]": { + "AddressMode": "LoRom", + "Size": 1048576, + "LoRom": "MDFDUSAgAAAAAAAAAAYAAFN0dW50IFJhY2UgRlggICAgICAgICAaCgABMwE3KMjX/////5D+kP6Q/ggBAAAMAf////+Q/gAAkP6Q/oj+kP4=", + "HiRom": "//////////////////////////////////////////////////////////////////////////////////////////////////////////8=", + "ExLoRom": null, + "ExHiRom": null + }, + "Wild Trax (J)": { + "AddressMode": "LoRom", + "Size": 1048576, + "LoRom": "MDFDUSAgAAAAAAAAAAYAAFdJTEQgVFJBWCAgICAgICAgICAgICAaCgAAMwG/QEC//////5D+kP6Q/ggBAAAMAf////+Q/gAAkP6Q/oj+kP4=", + "HiRom": "//////////////////////////////////////////////////////////////////////////////////////////////////////////8=", + "ExLoRom": null, + "ExHiRom": null + }, + "Akumajou Dracula (J) [!]": { + "AddressMode": "LoRom", + "Size": 1048576, + "LoRom": "/////////////////////0FLVU1BSk8gRFJBQ1VMQSAgICAgICAACgAApADbbCST/////76CvoK+griBAIC+gv////++gr6CvoK4gQCAvoI=", + "HiRom": "//////////////////////////////////////////////////////////////////////////////////////////////////////////8=", + "ExLoRom": null, + "ExHiRom": null + }, + "Super Castlevania IV (E) [!]": { + "AddressMode": "LoRom", + "Size": 1048576, + "LoRom": "/////////////////////1NVUEVSIENBU1RMRVZBTklBIDQgICAACgACpADFFTrq/////76CvoK+griBAIC+gv////++gr6CvoK4gQCAvoI=", + "HiRom": "//////////////////////////////////////////////////////////////////////////////////////////////////////////8=", + "ExLoRom": null, + "ExHiRom": null + }, + "Super Castlevania IV (U) [!]": { + "AddressMode": "LoRom", + "Size": 1048576, + "LoRom": "/////////////////////1NVUEVSIENBU1RMRVZBTklBIDQgICAACgABpADa/CUD/////76CvoK+griBAIC+gv////++gr6CvoK4gQCAvoI=", + "HiRom": "//////////////////////////////////////////////////////////////////////////////////////////////////////////8=", + "ExLoRom": null, + "ExHiRom": null + }, + "Super Mario 3 Expert (Hack)": { + "AddressMode": "LoRom", + "Size": 2097152, + "LoRom": "/////////////////////1NVUEVSIE1BUklPIEFMTF9TVEFSUyACCwMBAQCjVVyq//////6C///+gtiCAIBsg//////+gv6C/oL+ggCAbIM=", + "HiRom": "RgBHAO8ADwAHAAMAAQDhAP8Pf48/zz/PH++f/5//n/8P8A9wD7APsA/QH8AfwB/A//9//79/v3/fP98/3z/fP/AAcACwALAA0ADAAMAAwAA=", + "ExLoRom": null, + "ExHiRom": null + }, + "Super Mario All-Stars (E) [!]": { + "AddressMode": "LoRom", + "Size": 2097152, + "LoRom": "/////////////////////1NVUEVSIE1BUklPIEFMTF9TVEFSUyACCwMCAQBWVqmp//////6C///+gtiCAIBsg//////+gv6C/oL+ggCAbIM=", + "HiRom": "RgBHAO8ADwAHAAMAAQDhAP8Pf48/zz/PH++f/5//n/8P8A9wD7APsA/QH8AfwB/A//9//79/v3/fP98/3z/fP/AAcACwALAA0ADAAMAAwAA=", + "ExLoRom": null, + "ExHiRom": null + }, + "Super Mario All-Stars (U) [!] - 8MB": { + "AddressMode": "ExLoRom", + "Size": 8388608, + "LoRom": "/////////////////////1NVUEVSIE1BUklPIEFMTF9TVEFSUyACDQMBAQCjVVyq//////6C///+gtiCAIBsg//////+gv6C/oL+ggCAbIM=", + "HiRom": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", + "ExLoRom": "/////////////////////1NVUEVSIE1BUklPIEFMTF9TVEFSUyACDQMBAQCjVVyq//////6C///+gtiCAIBsg//////+gv6C/oL+ggCAbIM=", + "ExHiRom": "RgBHAO8ADwAHAAMAAQDhAP8Pf48/zz/PH++f/5//n/8P8A9wD7APsA/QH8AfwB/A//9//79/v3/fP98/3z/fP/AAcACwALAA0ADAAMAAwAA=" + }, + "Super Mario All-Stars (U) [!]": { + "AddressMode": "LoRom", + "Size": 2097152, + "LoRom": "/////////////////////1NVUEVSIE1BUklPIEFMTF9TVEFSUyACCwMBAQCjVVyq//////6C///+gtiCAIBsg//////+gv6C/oL+ggCAbIM=", + "HiRom": "RgBHAO8ADwAHAAMAAQDhAP8Pf48/zz/PH++f/5//n/8P8A9wD7APsA/QH8AfwB/A//9//79/v3/fP98/3z/fP/AAcACwALAA0ADAAMAAwAA=", + "ExLoRom": null, + "ExHiRom": null + }, + "Super Mario All-Stars + Super Mario World (E) [!]": { + "AddressMode": "LoRom", + "Size": 2621440, + "LoRom": "MDE1TSAgAAAAAAAAAAAAAE1BUklPX0FMTFNUQVJTK1dPUkxEICACDAMCMwAfFuDp/////zSD//80gw6DAICpg/////80gzSDNIM0gwCAqYM=", + "HiRom": "RgBHAO8ADwAHAAMAAQDhAP8Pf48/zz/PH++f/5//n/8P8A9wD7APsA/QH8AfwB/A//9//79/v3/fP98/3z/fP/AAcACwALAA0ADAAMAAwAA=", + "ExLoRom": null, + "ExHiRom": null + }, + "Super Mario All-Stars + Super Mario World (U) [!]": { + "AddressMode": "LoRom", + "Size": 2621440, + "LoRom": "/////////////////////0FMTF9TVEFSUyArIFdPUkxEICAgICACDAMBAQAnn9hg/////zSD//80gw6DAICpg/////80gzSDNIM0gwCAqYM=", + "HiRom": "RgBHAO8ADwAHAAMAAQDhAP8Pf48/zz/PH++f/5//n/8P8A9wD7APsA/QH8AfwB/A//9//79/v3/fP98/3z/fP/AAcACwALAA0ADAAMAAwAA=", + "ExLoRom": null, + "ExHiRom": null + }, + "Super Mario Bros. (U)": { + "AddressMode": "LoRom", + "Size": 524288, + "LoRom": "/////////////////////1N1cGVyIE1hcmlvIEJyb3MuIDEgICACCQMBAQBoWZem/////waF//8GheiCAIAHhf////8GhQaFBoUGhQCAB4U=", + "HiRom": "//////////////////////////////////////////////////////////////////////////////////////////////////////////8=", + "ExLoRom": null, + "ExHiRom": null + }, + "Super Mario Collection (J) (V1.0)": { + "AddressMode": "LoRom", + "Size": 2097152, + "LoRom": "/////////////////////1NVUEVSTUFSSU8gQ09MTEVDVElPTiACCwMAAQA1tMpL//////uC///7gtWCAIBpg//////7gvuC+4L7ggCAaYM=", + "HiRom": "RgBHAO8ADwAHAAMAAQDhAP8Pf48/zz/PH++f/5//n/8P8A9wD7APsA/QH8AfwB/A//9//79/v3/fP98/3z/fP/AAcACwALAA0ADAAMAAwAA=", + "ExLoRom": null, + "ExHiRom": null + }, + "Super Mario Collection (J) (V1.1)": { + "AddressMode": "LoRom", + "Size": 2097152, + "LoRom": "/////////////////////1NVUEVSTUFSSU8gQ09MTEVDVElPTiACCwMAAQHgVx+o//////uC///7gtWCAIBpg//////7gvuC+4L7ggCAaYM=", + "HiRom": "RgBHAO8ADwAHAAMAAQDhAP8Pf48/zz/PH++f/5//n/8P8A9wD7APsA/QH8AfwB/A//9//79/v3/fP98/3z/fP/AAcACwALAA0ADAAMAAwAA=", + "ExLoRom": null, + "ExHiRom": null + }, + "Test ROM": { + "AddressMode": "LoRom", + "Size": 2097152, + "LoRom": "/////////////////////1NVUEVSIE1BUklPIEFMTF9TVEFSUyACCwMBAQCjVVyq//////6C///+gtiCAIBsg//////+gv6C/oL+ggCAbIM=", + "HiRom": "RgBHAO8ADwAHAAMAAQDhAP8Pf48/zz/PH++f/5//n/8P8A9wD7APsA/QH8AfwB/A//9//79/v3/fP98/3z/fP/AAcACwALAA0ADAAMAAwAA=", + "ExLoRom": null, + "ExHiRom": null + }, + "Mario Kart R": { + "AddressMode": "HiRom", + "Size": 1048576, + "LoRom": "Hx9vIG9vLyAnYA9/AAAAAP/4//b/9v307+Tx/gAAAAD4+PYE9vb0BOQG8P4AAAAA/PgE+PwA/AD8APwA+AAAAAAAAAAAAAAAAAAAAAAAAAA=", + "HiRom": "AAAAAAAAAAAAAAAAAAAAAE1LUiBCWSBERk9SQ0UvRDRTICAgIDEFCgEBAQCJeHaHIAAAAI//ALkQAIX/CACK/wKoAAAJgAC5ikgAAHD/AAA=", + "ExLoRom": null, + "ExHiRom": null + }, + "Super Mario Kart (E) [!]": { + "AddressMode": "HiRom", + "Size": 524288, + "LoRom": "HwBvAG8ALwAnQA9wAAAAAAcA+/IJAPnw6+ABDv4AAAD4APYA9gD0AOQC8A4AAAAA/PgE+PwA/AD8APwA+AAAAAAAAAAAAAAAAAAAAAAAAAA=", + "HiRom": "AAAAAAAAAAAAAAAAAAAAAFNVUEVSIE1BUklPIEtBUlQgICAgIDEFCQECAQBHrbhSIAAAAI//ALkAAIX/CACK/wKIAAAJAAC5ikgAAHD/AAA=", + "ExLoRom": null, + "ExHiRom": null + }, + "Super Mario Kart (J)": { + "AddressMode": "HiRom", + "Size": 524288, + "LoRom": "HwBvAG8ALwAnQA9wAAAAAAcA+/IJAPnw6+ABDv4AAAD4APYA9gD0AOQC8A4AAAAA/PgE+PwA/AD8APwA+AAAAAAAAAAAAAAAAAAAAAAAAAA=", + "HiRom": "AAAAAAAAAAAAAAAAAAAAAFNVUEVSIE1BUklPIEtBUlQgICAgIDEFCQEAAQDcdSOKIgAAAI//ALkAAIX/CACK/wKoAAAJAAC5ikgAAHD/AAA=", + "ExLoRom": null, + "ExHiRom": null + }, + "Super Mario Kart (U) [!]": { + "AddressMode": "HiRom", + "Size": 524288, + "LoRom": "HwBvAG8ALwAnQA9wAAAAAAcA+/IJAPnw6+ABDv4AAAD4APYA9gD0AOQC8A4AAAAA/PgE+PwA/AD8APwA+AAAAAAAAAAAAAAAAAAAAAAAAAA=", + "HiRom": "AAAAAAAAAAAAAAAAAAAAAFNVUEVSIE1BUklPIEtBUlQgICAgIDEFCQEBAQC7FETrIAAAAI//ALkQAIX/CACK/wKoAAAJgAC5ikgAAHD/AAA=", + "ExLoRom": null, + "ExHiRom": null + }, + "SMRPG_GBARP_US_7.1.8_full_4121810938": { + "AddressMode": "LoRom", + "Size": 4194304, + "LoRom": "MDFBUldFAAAAAAAAAAAAAFNNUlBHLVIgNDEyMTgxMDkzOCAgICM1DAUBMwf9qwJUAAAAAL6CvoK+gggAvoIMAAAAAAC+gr6CvoIIAJD/DAA=", + "HiRom": "6MjGKBD0eoLs+6tg4fsR/EL8cfyT/PT8Rv2Z/fT9Jv5Y/p/+3/4//4H/uv9F/4f/wP8AAKgBqAGoARgAqAEcAAAAAACoAagBqAEYAKD/HAA=", + "ExLoRom": null, + "ExHiRom": null + }, + "Super Mario RPG - Legend of the Seven Stars (U) [!]": { + "AddressMode": "LoRom", + "Size": 4194304, + "LoRom": "MDFBUldFAAAAAAAAAAAAAFNVUEVSIE1BUklPIFJQRyAgICAgICM1DAUBMwBLxLQ7AAAAAL6CvoK+gggAvoIMAAAAAAC+gr6CvoIIAJD/DAA=", + "HiRom": "6MjGKBD0eoLs+6tg4fsR/EL8cfyT/PT8Rv2Z/fT9Jv5Y/p/+3/4//4H/uv9F/4f/wP8AAKgBqAGoARgAqAEcAAAAAACoAagBqAEYAKD/HAA=", + "ExLoRom": null, + "ExHiRom": null + }, + "Super Mario RPG Armageddon v10 (Hard)": { + "AddressMode": "LoRom", + "Size": 4194304, + "LoRom": "MDFBUldFAAAAAAAAAAAAAFNVUEVSIE1BUklPIFJQRyAgICAgICM1DAUBMwDuOxHEAAAAAL6CvoK+gggAvoIMAAAAAAC+gr6CvoIIAJD/DAA=", + "HiRom": "6MjGKBD0eoLs+6tg4fsR/EL8cfyT/PT8Rv2Z/fT9Jv5Y/p/+3/4//4H/uv9F/4f/wP8AAKgBqAGoARgAqAEcAAAAAACoAagBqAEYAKD/HAA=", + "ExLoRom": null, + "ExHiRom": null + }, + "Super Mario RPG Armageddon v10 (Normal)": { + "AddressMode": "LoRom", + "Size": 4194304, + "LoRom": "MDFBUldFAAAAAAAAAAAAAFNVUEVSIE1BUklPIFJQRyAgICAgICM1DAUBMwCvq1BUAAAAAL6CvoK+gggAvoIMAAAAAAC+gr6CvoIIAJD/DAA=", + "HiRom": "6MjGKBD0eoLs+6tg4fsR/EL8cfyT/PT8Rv2Z/fT9Jv5Y/p/+3/4//4H/uv9F/4f/wP8AAKgBqAGoARgAqAEcAAAAAACoAagBqAEYAKD/HAA=", + "ExLoRom": null, + "ExHiRom": null + }, + "Bowser's Strike Back": { + "AddressMode": "LoRom", + "Size": 4194304, + "LoRom": "////////////////////QkJvd3NlcidzIFN0cmlrZUJhY2sgICACDAEBAQUlX9qg/////8OC///DgmqBAIB0g//////DgsOCw4LDggCAw4I=", + "HiRom": "vdQUaQCNtxTaIp3MAfpg//////////////////////////////////////////////////////////////////////////////////////8=", + "ExLoRom": null, + "ExHiRom": null + }, + "Brutal Mario": { + "AddressMode": "LoRom", + "Size": 4194304, + "LoRom": "//////////////////9OT1NVUEVSIE1BUklPV09STEQgICAgICACDAEBAQAlX9qg/////8OC///DgmqBAIB0g//////DgsOCw4LDggCAw4I=", + "HiRom": "vdQUaQCNtxTaIp3MAfpg/7WeyRrwC7kLAynxCQuZCwNguQcDKfEJC5kHA2D///////////////////////////////////////////////8=", + "ExLoRom": null, + "ExHiRom": null + }, + "Demo World - The Legend Continues": { + "AddressMode": "ExHiRom", + "Size": 6291456, + "LoRom": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", + "HiRom": "//////////////////+4QlN1cGVyIERlbW8gV29ybGQgVExDICUCDQEBAQUlX9qg/////8OC///DgmqBAIB0g//////DgsOCw4LDggCAw4I=", + "ExLoRom": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", + "ExHiRom": "////////////////////QlN1cGVyIERlbW8gV29ybGQgVExDICUCDQEBAQUlX9qg/////8OC///DgmqBAIB0g//////DgsOCw4LDggCAw4I=" + }, + "Kaizo Kindergarten v1.09": { + "AddressMode": "LoRom", + "Size": 3145728, + "LoRom": "/////////////////////1NVUEVSIE1BUklPV09STEQgICAgICACDAEBAQC8b0OQ/////8OC///DgmqBAIB0g//////DgsOCw4LDggCAw4I=", + "HiRom": "vdQUaQCNtxTaIp3MAfpg//////////////////////////////////////////////////////////////////////////////////////8=", + "ExLoRom": null, + "ExHiRom": null + }, + "Luigi's Adventure": { + "AddressMode": "LoRom", + "Size": 2097152, + "LoRom": "////////////////////Qkx1aWdpJ3MgQWR2ZW50dXJlICAgICACCwEBAQUlX9qg/////8OC///DgmqBAIB0g//////DgsOCw4LDggCAw4I=", + "HiRom": "vdQUaQCNtxTaIp3MAfpg//////////////////////////////////////////////////////////////////////////////////////8=", + "ExLoRom": null, + "ExHiRom": null + }, + "Mario Is Missing 2": { + "AddressMode": "LoRom", + "Size": 2097152, + "LoRom": "////////////////////Qkx1aWdpIFN0YXJ0ICEgICAgICAgICACCwEBAQVxHo7h/////8OC///DgmqBAIB0g//////DgsOCw4LDggCAw4I=", + "HiRom": "vdQUaQCNtxTaIp3MAfpg//////////////////////////////////////////////////////////////////////////////////////8=", + "ExLoRom": null, + "ExHiRom": null + }, + "Mario's Return": { + "AddressMode": "LoRom", + "Size": 4194304, + "LoRom": "4P0Da///////////////Qk1hcmlvJ3MgUmV0dXJuICAgICAgIDACDAEBAQUlX9qg/////8OC///Dgqf/AIB0g//////DgsOCw4LDgpP/w4I=", + "HiRom": "vdQUaQCNtxTaIp3MAfpg/whIIPD/8AZoKHm062BoKHn+/2AISCDw//AGaCjZz/hgaCjZ/P9g/////////////7+eAH7JKGD//////yDgBPw=", + "ExLoRom": null, + "ExHiRom": null + }, + "MarioX World Chronicles": { + "AddressMode": "LoRom", + "Size": 2097152, + "LoRom": "////////////////////Qk1hcmlvWCBXb3JsZCBTRVIgICAgICACCwEBAQUlX9qg/////8OC///DgmqBAIB0g//////DgsOCw4LDggCAw4I=", + "HiRom": "vdQUaQCNtxTaIp3MAfpg//////////////////////////////////////////////////////////////////////////////////////8=", + "ExLoRom": null, + "ExHiRom": null + }, + "Panic In the Mushroom Kingdom": { + "AddressMode": "LoRom", + "Size": 4194304, + "LoRom": "////////////////////QlBpdG1rIDEuMSAgICAgICAgICAgICACDAEBAQUlX9qg/////8OC///DgmqBAIB0g//////DgsOCw4LDggCAw4I=", + "HiRom": "vdQUaQCNtxTaIp3MAfpg////////////////////////////////DP////////////////////////////////////////////////////8=", + "ExLoRom": null, + "ExHiRom": null + }, + "Return to Dinosaur Land": { + "AddressMode": "LoRom", + "Size": 2097152, + "LoRom": "4P0Da///////////////QlJldHVybnRvRGlub3NhdXJMYW5kIDACCwEBAQUlX9qg/////8OC///Dgqf/AIB0g//////DgsOCw4LDgpP/w4I=", + "HiRom": "vdQUaQCNtxTaIp3MAfpg//////////////////////////////////////////////////////////////////////////////////////8=", + "ExLoRom": null, + "ExHiRom": null + }, + "SMW - HELL Edition": { + "AddressMode": "LoRom", + "Size": 1048576, + "LoRom": "/////////////////////1NNVyBIRUxMIEVESVRJT04gICAgICACCgEBAQAlX9qg/////8OC///DgmqBAIB0g//////DgsOCw4LDggCAw4I=", + "HiRom": "vdQUaQCNtxTaIp3MAfpg//////////////////////////////////////////////////////////////////////////////////////8=", + "ExLoRom": null, + "ExHiRom": null + }, + "Super Mario Islands": { + "AddressMode": "LoRom", + "Size": 4194304, + "LoRom": "4P0Da////////////////1NVUEVSIE1BUklPV09STEQgICAgIDACDAEBAQAlX9qg/////8OC///Dgqf/AIB0g//////DgsOCw4LDgpP/w4I=", + "HiRom": "vdQUaQCNtxTaIp3MAfpg//////////////////////////////////////////////////////////////////////////////////////8=", + "ExLoRom": null, + "ExHiRom": null + }, + "Super Mario World (U) [!] - 8MB Hack": { + "AddressMode": "ExLoRom", + "Size": 8388608, + "LoRom": "/////////////////////1NVUEVSIE1BUklPV09STEQgICAgICACDQEBAQAlX9qg/////8OC///DgmqBAIB0g//////DgsOCw4LDggCAw4I=", + "HiRom": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", + "ExLoRom": "/////////////////////1NVUEVSIE1BUklPV09STEQgICAgICACDQEBAQAlX9qg/////8OC///DgmqBAIB0g//////DgsOCw4LDggCAw4I=", + "ExHiRom": "vdQUaQCNtxTaIp3MAfpg//////////////////////////////////////////////////////////////////////////////////////8=" + }, + "Super Mario World (U) [!]": { + "AddressMode": "LoRom", + "Size": 524288, + "LoRom": "/////////////////////1NVUEVSIE1BUklPV09STEQgICAgICACCQEBAQAlX9qg/////8OC///DgmqBAIB0g//////DgsOCw4LDggCAw4I=", + "HiRom": "vdQUaQCNtxTaIp3MAfpg//////////////////////////////////////////////////////////////////////////////////////8=", + "ExLoRom": null, + "ExHiRom": null + }, + "Super Mario World R 2": { + "AddressMode": "LoRom", + "Size": 2097152, + "LoRom": "////////////////////QlNNV1IyICAgICAgICAgICAgICAgICACCwEBAQUlX9qg/////8OC///DgmqBAIB0g//////DgsOCw4LDggCAw4I=", + "HiRom": "vdQUaQCNtxTaIp3MAfpg//////////////////////////////////////////////////////////////////////////////////////8=", + "ExLoRom": null, + "ExHiRom": null + }, + "Super Mario World R EX": { + "AddressMode": "LoRom", + "Size": 4194304, + "LoRom": "////////////////////QlNNV1JFWCAgICAgICAgICAgICAgICACDAEBAQUlX9qg/////8OC///DgmqBAIB0g//////DgsOCw4LDggCAw4I=", + "HiRom": "vdQUaQCNtxTaIp3MAfpg//////////////////////////////////////////////////////////////////////////////////////8=", + "ExLoRom": null, + "ExHiRom": null + }, + "Super Mario World R": { + "AddressMode": "LoRom", + "Size": 2097152, + "LoRom": "////////////////////QlNNV1IgICAgICAgICAgICAgICAgICACCwEBAQUlX9qg/////8OC///DgmqBAIB0g//////DgsOCw4LDggCAw4I=", + "HiRom": "vdQUaQCNtxTaIp3MAfpg//////////////////////////////////////////////////////////////////////////////////////8=", + "ExLoRom": null, + "ExHiRom": null + }, + "The Adventures of Phantasm World": { + "AddressMode": "LoRom", + "Size": 4194304, + "LoRom": "////////////////////Qkx1aWdpJ3MgQWR2ZW50dXJlMiAgICACDAEBAQUlX9qg/////8OC///DgmqBAIB0g//////DgsOCw4LDggCAw4I=", + "HiRom": "vdQUaQCNtxTaIp3MAfpg//////////////////////////////////////////////////////////////////////////////////////8=", + "ExLoRom": null, + "ExHiRom": null + }, + "The Second Reality Project (Hard Type)": { + "AddressMode": "LoRom", + "Size": 2097152, + "LoRom": "////////////////////QlNSMSBWZXJzaW9uIDEtNSAgICAgICACCwEBAQUlX9qg/////8OC///DgmqBAIB0g//////DgsOCw4LDggCAw4I=", + "HiRom": "vdQUaQCNtxTaIp3MAfpg//////////////////////////////////////////////////////////////////////////////////////8=", + "ExLoRom": null, + "ExHiRom": null + }, + "The Second Reality Project (SNES Type)": { + "AddressMode": "LoRom", + "Size": 2097152, + "LoRom": "////////////////////QlNSUEkgU05FUy1WZXJzaW9uICAgICACCwEBAQUlX9qg/////8OC///DgmqBAIB0g//////DgsOCw4LDggCAw4I=", + "HiRom": "vdQUaQCNtxTaIp3MAfpg//////////////////////////////////////////////////////////////////////////////////////8=", + "ExLoRom": null, + "ExHiRom": null + }, + "VLDC11_BaseROM": { + "AddressMode": "LoRom", + "Size": 2097152, + "LoRom": "/////////////////////1NVUEVSIE1BUklPV09STEQgICAgICM1CwcBAQB+/4EA/////8OC///DgmqBAIAhHf/////DgsOCw4LDgnCKw4I=", + "HiRom": "vVgyaQCNt3TaIp3MAfpg//////////////////////////////////////////////////////////////////////////////////////8=", + "ExLoRom": null, + "ExHiRom": null + }, + "Super Mario - Yoshi Island (J) (V1.0)": { + "AddressMode": "LoRom", + "Size": 2097152, + "LoRom": "MDFZSSAgAAAAAAAAAAUAAFlPU1NZJ1MgSVNMQU5EICAgICAgICAVCwAAMwDxjA5zT4FPgU+BT4FPgQgBT4EMAU+BT4FPgU+BT4FPgQCAT4E=", + "HiRom": "//////////////////////////////////////////////////////////////////////////////////////////////////////////8=", + "ExLoRom": null, + "ExHiRom": null + }, + "Super Mario - Yoshi Island (J) (V1.1) [!]": { + "AddressMode": "LoRom", + "Size": 2097152, + "LoRom": "MDFZSSAgAAAAAAAAAAUAAFlPU1NZJ1MgSVNMQU5EICAgICAgICAVCwAAMwEyvM1DT4FPgU+BT4FPgQgBT4EMAU+BT4FPgU+BT4FPgQCAT4E=", + "HiRom": "/v7+//7+/v/+/v7//v7+//7+/v/+/v7//v7+//7+/v/+/v7//v7+//7+/v/+/v7//v7+//7+/v/+/v7//v7+//7+/v/+/v7//v7+//7+/v8=", + "ExLoRom": null, + "ExHiRom": null + }, + "Super Mario - Yoshi Island (J) (V1.2)": { + "AddressMode": "LoRom", + "Size": 2097152, + "LoRom": "MDFZSSAgAAAAAAAAAAUAAFlPU1NZJ1MgSVNMQU5EICAgICAgICAVCwAAMwJtvJJDT4FPgU+BT4FPgQgBT4EMAU+BT4FPgU+BT4FPgQCAT4E=", + "HiRom": "/v7+//7+/v/+/v7//v7+//7+/v/+/v7//v7+//7+/v/+/v7//v7+//7+/v/+/v7//v7+//7+/v/+/v7//v7+//7+/v/+/v7//v7+//7+/v8=", + "ExLoRom": null, + "ExHiRom": null + }, + "Super Mario World 2 - Yoshi's Island (E) (V1.0) [!]": { + "AddressMode": "LoRom", + "Size": 2097152, + "LoRom": "MDFZSSAgAAAAAAAAAAUAAFlPU0hJJ1MgSVNMQU5EICAgICAgICAVCwACMwD0HgvhT4FPgU+BT4FPgQgBT4EMAU+BT4FPgU+BT4FPgQCAT4E=", + "HiRom": "//////////////////////////////////////////////////////////////////////////////////////////////////////////8=", + "ExLoRom": null, + "ExHiRom": null + }, + "Super Mario World 2 - Yoshi's Island (E) (V1.1)": { + "AddressMode": "LoRom", + "Size": 2097152, + "LoRom": "MDFZSSAgAAAAAAAAAAUAAFlPU0hJJ1MgSVNMQU5EICAgICAgICAVCwACMwGrzlQxT4FPgU+BT4FPgQgBT4EMAU+BT4FPgU+BT4FPgQCAT4E=", + "HiRom": "/v7+//7+/v/+/v7//v7+//7+/v/+/v7//v7+//7+/v/+/v7//v7+//7+/v/+/v7//v7+//7+/v/+/v7//v7+//7+/v/+/v7//v7+//7+/v8=", + "ExLoRom": null, + "ExHiRom": null + }, + "Super Mario World 2 - Yoshi's Island (U) (V1.0) [!]": { + "AddressMode": "LoRom", + "Size": 2097152, + "LoRom": "MDFZSSAgAAAAAAAAAAUAAFlPU0hJJ1MgSVNMQU5EICAgICAgICAVCwABMwDT7CwTT4FPgU+BT4FPgQgBT4EMAU+BT4FPgU+BT4FPgQCAT4E=", + "HiRom": "//////////////////////////////////////////////////////////////////////////////////////////////////////////8=", + "ExLoRom": null, + "ExHiRom": null + }, + "Super Mario World 2 - Yoshi's Island (U) (V1.1)": { + "AddressMode": "LoRom", + "Size": 2097152, + "LoRom": "MDFZSSAgAAAAAAAAAAUAAFlPU0hJJ1MgSVNMQU5EICAgICAgICAVCwABMwHNoTJeT4FPgU+BT4FPgQgBT4EMAU+BT4FPgU+BT4FPgQCAT4E=", + "HiRom": "/v7+//7+/v/+/v7//v7+//7+/v/+/v7//v7+//7+/v/+/v7//v7+//7+/v/+/v7//v7+//7+/v/+/v7//v7+//7+/v/+/v7//v7+//7+/v8=", + "ExLoRom": null, + "ExHiRom": null + }, + "Super MarioWorld 2+": { + "AddressMode": "LoRom", + "Size": 2097152, + "LoRom": "MDFZSSAgAAAAAAAAAAUAAFlPU0hJJ1MgSVNMQU5EICAgICAgICAVCwABMwDT7CwTT4FPgU+BT4FPgQgBT4EMAU+BT4FPgU+BT4FPgQCAT4E=", + "HiRom": "//////////////////////////////////////////////////////////////////////////////////////////////////////////8=", + "ExLoRom": null, + "ExHiRom": null + }, + "Super Metroid (E) [!]": { + "AddressMode": "LoRom", + "Size": 3145728, + "LoRom": "/////////////////////1N1cGVyIE1ldHJvaWQgICAgICAgIDACDAMCAQAEhPt7c4VzhXOFc4VzhYOVc4VqmHOFc4VzhXOFc4VzhRyEc4U=", + "HiRom": "//////////////////////////////////////////////////////////////////////////////////////////////////////////8=", + "ExLoRom": null, + "ExHiRom": null + }, + "Super Metroid (JU) [!]": { + "AddressMode": "LoRom", + "Size": 3145728, + "LoRom": "/////////////////////1N1cGVyIE1ldHJvaWQgICAgICAgIDACDAMAAQAgB9/4c4VzhXOFc4VzhYOVc4VqmHOFc4VzhXOFc4VzhRyEc4U=", + "HiRom": "//////////////////////////////////////////////////////////////////////////////////////////////////////////8=", + "ExLoRom": null, + "ExHiRom": null + }, + "Tales of Phantasia (J) [!]": { + "AddressMode": "ExHiRom", + "Size": 6291456, + "LoRom": "qQGNC0IidYfAsKSpAY0AQ6kYjQFDogAQjgJDqX+NBEOiABCOBUOiAHiOFiGpAY0LQiAThSKT58MidYfAkAYg54ZMBIGpAY0hIZwiIZwiIZw=", + "HiRom": "QUZBVFZKAAAAAAAAAAAAAFRBTEVTIE9GIFBIQU5UQVNJQSAgIDUCDQMAMwD//wAAAAAAAALwAvAC8BLwAPAW8AAAAAAC8ALwAvAS8BrwFvA=", + "ExLoRom": "IMSDA7oPbtoPBxSA6/9/A4B3Po8AIY0cAIFRl4Dg4MCGQOFA4UDgAgfMI++fEL9A7xAGGy8g78T8eyAXEPybQODADCDtfxLuEfkG4R5PJ+U=", + "ExHiRom": "QUZBVFZKAAAAAAAAAAAAAFRBTEVTIE9GIFBIQU5UQVNJQSAgIDUCDQMAMwCog1d8AAAAAALwAvAC8BLwAPAW8AAAAAAC8ALwAvAS8BrwFvA=" + }, + "Tetris & Dr. Mario (E) [!]": { + "AddressMode": "LoRom", + "Size": 1048576, + "LoRom": "MDFBVEZQAAAAAAAAAAAAAFRFVFJJUyZEci5NQVJJTyAgICAgIDAACgACMwBl+ZoG//+A/uL/4v/i/+yIAADOiQAAAADi/wAA4v/siACAzok=", + "HiRom": "//////////////////////////////////////////////////////////////////////////////////////////////////////////8=", + "ExLoRom": null, + "ExHiRom": null + }, + "Tetris & Dr. Mario (U) [!]": { + "AddressMode": "LoRom", + "Size": 1048576, + "LoRom": "MDFBVEZFAAAAAAAAAAAAAFRFVFJJUyZEci5NQVJJTyAgICAgIDAACgABMwCf+mAF//+A/uL/4v/i/+yIAADOiQAAAADi/wAA4v/siACAzok=", + "HiRom": "//////////////////////////////////////////////////////////////////////////////////////////////////////////8=", + "ExLoRom": null, + "ExHiRom": null + }, + "BS Panel de Pon Event '98 (J)": { + "AddressMode": "LoRom", + "Size": 1048576, + "LoRom": "MDFBWUxKAAAAAAAAAAAAAMrfyNmCxc7f3Sc5OCAgICD/AAAAAAAQWzAQMwHhfR6CAIAAgACAAIAAgN6KAICBiwCAAIAAgACAAIAAgAKAAIA=", + "HiRom": "CAAAqn0BAOMB87U/AQDjAfO0PwIA4gHqwD/qgfBkPwIA4gHqwT/qgfCEPwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", + "ExLoRom": null, + "ExHiRom": null + }, + "BS Yoshi no Panepon (J)": { + "AddressMode": "LoRom", + "Size": 1048576, + "LoRom": "MDFBWUxFAAAAAAAAAAAAANavvLCCzMrfyM7f3UJTlMX/////AABwGCAQMwJ5+oYFAIAAgACAAIAAgHWNAIANjgCAAIAAgACAAIAAgAKAAIA=", + "HiRom": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", + "ExLoRom": null, + "ExHiRom": null + }, + "Panel de Pon (J)": { + "AddressMode": "LoRom", + "Size": 1048576, + "LoRom": "MDFBWUxKAAAAAAAAAAAAAMrfyNkgw94gzt/dICAgICAgICAgIDAACgAAMwALMfTOAIAAgACAAIAAgBqLAIC9iwCAAIAAgACAAIAAgAKAAIA=", + "HiRom": "CAAAqn0BAOMB87U/AQDjAfO0PwIA4gHqwD/qgfBkPwIA4gHqwT/qgfCEPwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", + "ExLoRom": null, + "ExHiRom": null + }, + "Tetris Attack (E)": { + "AddressMode": "LoRom", + "Size": 1048576, + "LoRom": "MDFBWUxQAAAAAAAAAAAAAFRFVFJJUyBBVFRBQ0sgICAgICAgIDAACgACMwAqYdWeAIAAgACAAIAAgCGOAIC5jgCAAIAAgACAAIAAgAKAAIA=", + "HiRom": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", + "ExLoRom": null, + "ExHiRom": null + }, + "Tetris Attack (U) [!]": { + "AddressMode": "LoRom", + "Size": 1048576, + "LoRom": "MDFBWUxFAAAAAAAAAAAAAFRFVFJJUyBBVFRBQ0sgICAgICAgIDAACgABMwAM0/MsAIAAgACAAIAAgCGOAIC5jgCAAIAAgACAAIAAgAKAAIA=", + "HiRom": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", + "ExLoRom": null, + "ExHiRom": null + }, + "Tetris Attack (U) [f1]": { + "AddressMode": "LoRom", + "Size": 1048576, + "LoRom": "MDFBWUxFAAAAAAAAAAAAAFRFVFJJUyBBVFRBQ0sgICAgICAgIDAACgABMwAM0/MsAIAAgACAAIAAgCGOAIC5jgCAAIAAgACAAIAAgAKAAIA=", + "HiRom": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", + "ExLoRom": null, + "ExHiRom": null + }, + "Tetris Attack (U) [T+Spa050_A2j]": { + "AddressMode": "LoRom", + "Size": 1048576, + "LoRom": "MDFBWUxFAAAAAAAAAAAAAFRFVFJJUyBBVFRBQ0sgICAgICAgIDAACgABMwAM0/MsAIAAgACAAIAAgCGOAIC5jgCAAIAAgACAAIAAgAKAAIA=", + "HiRom": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", + "ExLoRom": null, + "ExHiRom": null + }, + "Uniracers (U) [!]": { + "AddressMode": "LoRom", + "Size": 2097152, + "LoRom": "MDE0TCAgAAAAAAAAAAAAAFVOSVJBQ0VSUyAgICAgICAgICAgIDACCwMBAQBk/JsDAAasLQAGgIUABoiFAAaEhQAGpC0ABqQtAAakLViIhIU=", + "HiRom": "//////////////////////////////////////////////////////////////////////////////////////////////////////////8=", + "ExLoRom": null, + "ExHiRom": null + }, + "Unirally (E) [!]": { + "AddressMode": "LoRom", + "Size": 2097152, + "LoRom": "MDE0TCAgAAAAAAAAAAAAAFVOSVJBTExZICAgICAgICAgICAgIDACCwMCMwAsddOK////////f4X//4eF//+Dhf///////////////1iIg4U=", + "HiRom": "//////////////////////////////////////////////////////////////////////////////////////////////////////////8=", + "ExLoRom": null, + "ExHiRom": null + } +} \ No newline at end of file diff --git a/test/Snes.Tests/RomTests.cs b/test/Snes.Tests/RomTests.cs new file mode 100644 index 0000000..ec5c835 --- /dev/null +++ b/test/Snes.Tests/RomTests.cs @@ -0,0 +1,1088 @@ +namespace Maseya.Snes.Tests; + +using System; +using System.ComponentModel; +using System.Text; +using System.Text.Json; + +using Xunit; + +public class RomTests +{ + private static Dictionary? _testRoms = null; + + private static Dictionary TestRoms + { + get + { + _testRoms ??= JsonSerializer.Deserialize>( + Properties.Resources.RomFormatTestData)!; + + return _testRoms; + } + } + + [Fact] + public void SnesToPc_InvalidAddressMode_ThrowsInvalidEnumException() + { + _ = Assert.Throws( + () => _ = Rom.SnesToPc(0x8000, false, (AddressMode)(-1))); + } + + [Theory] + [InlineData(0x008000, 0x000000)] + [InlineData(0x00FFFF, 0x007FFF)] + [InlineData(0x018000, 0x008000)] + [InlineData(0x6FFFFF, 0x37FFFF)] + public void SnesToPc_ValidLoRomAddress_ReturnsValidPcAddress( + int snesAddress, + int expectedPcAddress) + { + SnesToPc_ValidInput_ReturnsValidAddress( + snesAddress, + false, + AddressMode.LoRom, + expectedPcAddress); + } + + [Theory] + [InlineData(0x008000, 0x000000)] + [InlineData(0x00FFFF, 0x007FFF)] + [InlineData(0x018000, 0x008000)] + [InlineData(0x6FFFFF, 0x37FFFF)] + [InlineData(0x808000, 0x000000)] + [InlineData(0x80FFFF, 0x007FFF)] + [InlineData(0x818000, 0x008000)] + [InlineData(0xEFFFFF, 0x37FFFF)] + public void SnesToPc_ValidLoRom2Address_ReturnsValidPcAddress( + int snesAddress, + int expectedPcAddress) + { + SnesToPc_ValidInput_ReturnsValidAddress( + snesAddress, + false, + AddressMode.LoRom2, + expectedPcAddress); + } + + [Theory] + [InlineData(0x008000, 0x008000)] + [InlineData(0x00FFFF, 0x00FFFF)] + [InlineData(0x018000, 0x018000)] + [InlineData(0x3FFFFF, 0x3FFFFF)] + [InlineData(0x400000, 0x000000)] + [InlineData(0x407FFF, 0x007FFF)] + [InlineData(0x408000, 0x008000)] + [InlineData(0x6FFFFF, 0x2FFFFF)] + [InlineData(0x808000, 0x008000)] + [InlineData(0xBFFFFF, 0x3FFFFF)] + [InlineData(0xC00000, 0x000000)] + [InlineData(0xC07FFF, 0x007FFF)] + [InlineData(0xC08000, 0x008000)] + [InlineData(0xFFFFFF, 0x3FFFFF)] + public void SnesToPc_ValidHiRomAddress_ReturnsValidPcAddress( + int snesAddress, + int expectedPcAddress) + { + SnesToPc_ValidInput_ReturnsValidAddress( + snesAddress, + false, + AddressMode.HiRom, + expectedPcAddress); + } + + [Theory] + [InlineData(0x008000, 0x008000)] + [InlineData(0x00FFFF, 0x00FFFF)] + [InlineData(0x018000, 0x018000)] + [InlineData(0x3FFFFF, 0x3FFFFF)] + [InlineData(0x400000, 0x000000)] + [InlineData(0x407FFF, 0x007FFF)] + [InlineData(0x408000, 0x008000)] + [InlineData(0x6FFFFF, 0x2FFFFF)] + [InlineData(0x808000, 0x008000)] + [InlineData(0xBFFFFF, 0x3FFFFF)] + [InlineData(0xC00000, 0x000000)] + [InlineData(0xC07FFF, 0x007FFF)] + [InlineData(0xC08000, 0x008000)] + [InlineData(0xFFFFFF, 0x3FFFFF)] + public void SnesToPc_ValidHiRom2Address_ReturnsValidPcAddress( + int snesAddress, + int expectedPcAddress) + { + SnesToPc_ValidInput_ReturnsValidAddress( + snesAddress, + false, + AddressMode.HiRom2, + expectedPcAddress); + } + + [Theory] + [InlineData(0x008000, 0x400000)] + [InlineData(0x018000, 0x408000)] + [InlineData(0x7DFFFF, 0x7EFFFF)] + [InlineData(0x808000, 0x000000)] + [InlineData(0x818000, 0x008000)] + [InlineData(0xFFFFFF, 0x3FFFFF)] + public void SnesToPc_ValidExLoRomAddress_ReturnsValidPcAddress( + int snesAddress, + int expectedPcAddress) + { + SnesToPc_ValidInput_ReturnsValidAddress( + snesAddress, + false, + AddressMode.ExLoRom, + expectedPcAddress); + } + + [Theory] + [InlineData(0x008000, 0x408000)] + [InlineData(0x3FFFFF, 0x7FFFFF)] + [InlineData(0x400000, 0x400000)] + [InlineData(0x408000, 0x408000)] + [InlineData(0x6FFFFF, 0x6FFFFF)] + [InlineData(0x808000, 0x408000)] + [InlineData(0xBFFFFF, 0x7FFFFF)] + [InlineData(0xC00000, 0x000000)] + [InlineData(0xFFFFFF, 0x3FFFFF)] + public void SnesToPc_ValidExHiRomAddress_ReturnsValidPcAddress( + int snesAddress, + int expectedPcAddress) + { + SnesToPc_ValidInput_ReturnsValidAddress( + snesAddress, + false, + AddressMode.ExHiRom, + expectedPcAddress); + } + + [Theory] + [InlineData(0x008000, 0x000200)] + [InlineData(0x6FFFFF, 0x3801FF)] + public void SnesToPc_ValidHeaderLoRomAddress_ReturnsValidPcAddress( + int snesAddress, + int expectedPcAddress) + { + SnesToPc_ValidInput_ReturnsValidAddress( + snesAddress, + true, + AddressMode.LoRom, + expectedPcAddress); + } + + [Theory] + [InlineData(0x008000, 0x000200)] + [InlineData(0x6FFFFF, 0x3801FF)] + public void SnesToPc_ValidHeaderLoRom2Address_ReturnsValidPcAddress( + int snesAddress, + int expectedPcAddress) + { + SnesToPc_ValidInput_ReturnsValidAddress( + snesAddress, + true, + AddressMode.LoRom2, + expectedPcAddress); + } + + [Theory] + [InlineData(0x008000, 0x008200)] + [InlineData(0x3FFFFF, 0x4001FF)] + [InlineData(0x400000, 0x000200)] + [InlineData(0x6FFFFF, 0x3001FF)] + [InlineData(0x808000, 0x008200)] + [InlineData(0xBFFFFF, 0x4001FF)] + [InlineData(0xC00000, 0x000200)] + [InlineData(0xFFFFFF, 0x4001FF)] + public void SnesToPc_ValidHeaderHiRomAddress_ReturnsValidPcAddress( + int snesAddress, + int expectedPcAddress) + { + SnesToPc_ValidInput_ReturnsValidAddress( + snesAddress, + true, + AddressMode.HiRom, + expectedPcAddress); + } + + [Theory] + [InlineData(0x3FFFFF, 0x4001FF)] + [InlineData(0x400000, 0x000200)] + [InlineData(0x6FFFFF, 0x3001FF)] + [InlineData(0x808000, 0x008200)] + [InlineData(0xBFFFFF, 0x4001FF)] + [InlineData(0xC00000, 0x000200)] + [InlineData(0xFFFFFF, 0x4001FF)] + public void SnesToPc_ValidHeaderHiRom2Address_ReturnsValidPcAddress( + int snesAddress, + int expectedPcAddress) + { + SnesToPc_ValidInput_ReturnsValidAddress( + snesAddress, + true, + AddressMode.HiRom2, + expectedPcAddress); + } + + [Theory] + [InlineData(0x808000, 0x000200)] + [InlineData(0xFFFFFF, 0x4001FF)] + [InlineData(0x6FFFFF, 0x7801FF)] + public void SnesToPc_ValidHeaderExLoRomAddress_ReturnsValidPcAddress( + int snesAddress, + int expectedPcAddress) + { + SnesToPc_ValidInput_ReturnsValidAddress( + snesAddress, + true, + AddressMode.ExLoRom, + expectedPcAddress); + } + + [Theory] + [InlineData(0x008000, 0x408200)] + [InlineData(0x3FFFFF, 0x8001FF)] + [InlineData(0xBFFFFF, 0x8001FF)] + [InlineData(0xC00000, 0x000200)] + [InlineData(0xFFFFFF, 0x4001FF)] + public void SnesToPc_ValidHeaderExHiRomAddress_ReturnsValidPcAddress( + int snesAddress, + int expectedPcAddress) + { + SnesToPc_ValidInput_ReturnsValidAddress( + snesAddress, + true, + AddressMode.ExHiRom, + expectedPcAddress); + } + + [Fact] + public void PcToSnes_InvalidAddressMode_ThrowsInvalidEnumException() + { + static void action() + { + _ = Rom.PcToSnes(0, false, (AddressMode)(-1)); + } + + _ = Assert.Throws(action); + } + + [Theory] + [InlineData(0x000000, 0x008000)] + [InlineData(0x007FFF, 0x00FFFF)] + [InlineData(0x008000, 0x018000)] + [InlineData(0x37FFFF, 0x6FFFFF)] + [InlineData(0x3FFFFF, 0xFFFFFF)] + public void PcToSnes_ValidLoRomAddress_ReturnsValidPcAddress( + int snesAddress, + int expectedPcAddress) + { + PcToSnes_ValidInput_ReturnsValidAddress( + snesAddress, + false, + AddressMode.LoRom, + expectedPcAddress); + } + + [Theory] + [InlineData(0x000000, 0x808000)] + [InlineData(0x007FFF, 0x80FFFF)] + [InlineData(0x008000, 0x818000)] + [InlineData(0x37FFFF, 0xEFFFFF)] + [InlineData(0x3FFFFF, 0xFFFFFF)] + public void PcToSnes_ValidLoRom2Address_ReturnsValidPcAddress( + int snesAddress, + int expectedPcAddress) + { + PcToSnes_ValidInput_ReturnsValidAddress( + snesAddress, + false, + AddressMode.LoRom2, + expectedPcAddress); + } + + [Theory] + [InlineData(0x000000, 0xC00000)] + [InlineData(0x3FFFFF, 0xFFFFFF)] + public void PcToSnes_ValidHiRomAddress_ReturnsValidPcAddress( + int snesAddress, + int expectedPcAddress) + { + PcToSnes_ValidInput_ReturnsValidAddress( + snesAddress, + false, + AddressMode.HiRom, + expectedPcAddress); + } + + [Theory] + [InlineData(0x000000, 0x400000)] + [InlineData(0x2FFFFF, 0x6FFFFF)] + [InlineData(0x300000, 0xF00000)] + [InlineData(0x3FFFFF, 0xFFFFFF)] + public void PcToSnes_ValidHiRom2Address_ReturnsValidPcAddress( + int snesAddress, + int expectedPcAddress) + { + PcToSnes_ValidInput_ReturnsValidAddress( + snesAddress, + false, + AddressMode.HiRom2, + expectedPcAddress); + } + + [Theory] + [InlineData(0x000000, 0x808000)] + [InlineData(0x008000, 0x818000)] + [InlineData(0x3FFFFF, 0xFFFFFF)] + [InlineData(0x400000, 0x008000)] + [InlineData(0x700000, 0x608000)] + [InlineData(0x7EFFFF, 0x7DFFFF)] + public void PcToSnes_ValidExLoRomAddress_ReturnsValidPcAddress( + int snesAddress, + int expectedPcAddress) + { + PcToSnes_ValidInput_ReturnsValidAddress( + snesAddress, + false, + AddressMode.ExLoRom, + expectedPcAddress); + } + + [Theory] + [InlineData(0x000000, 0xC00000)] + [InlineData(0x3FFFFF, 0xFFFFFF)] + [InlineData(0x400000, 0x400000)] + [InlineData(0x6FFFFF, 0x6FFFFF)] + [InlineData(0x708000, 0x708000)] + [InlineData(0x778000, 0x778000)] + [InlineData(0x780000, 0x780000)] + [InlineData(0x7DFFFF, 0x7DFFFF)] + [InlineData(0x7E8000, 0x3E8000)] + [InlineData(0x7FFFFF, 0x3FFFFF)] + public void PcToSnes_ValidExHiRomAddress_ReturnsValidPcAddress( + int snesAddress, + int expectedPcAddress) + { + PcToSnes_ValidInput_ReturnsValidAddress( + snesAddress, + false, + AddressMode.ExHiRom, + expectedPcAddress); + } + + [Theory] + [InlineData(0x000200, 0x008000)] + [InlineData(0x4001FF, 0xFFFFFF)] + public void PcToSnes_ValidHeaderLoRomAddress_ReturnsValidPcAddress( + int snesAddress, + int expectedPcAddress) + { + PcToSnes_ValidInput_ReturnsValidAddress( + snesAddress, + true, + AddressMode.LoRom, + expectedPcAddress); + } + + [Theory] + [InlineData(0x000200, 0x808000)] + [InlineData(0x4001FF, 0xFFFFFF)] + public void PcToSnes_ValidHeaderLoRom2Address_ReturnsValidPcAddress( + int snesAddress, + int expectedPcAddress) + { + PcToSnes_ValidInput_ReturnsValidAddress( + snesAddress, + true, + AddressMode.LoRom2, + expectedPcAddress); + } + + [Theory] + [InlineData(0x000200, 0xC00000)] + [InlineData(0x4001FF, 0xFFFFFF)] + public void PcToSnes_ValidHeaderHiRomAddress_ReturnsValidPcAddress( + int snesAddress, + int expectedPcAddress) + { + PcToSnes_ValidInput_ReturnsValidAddress( + snesAddress, + true, + AddressMode.HiRom, + expectedPcAddress); + } + + [Theory] + [InlineData(0x000200, 0x400000)] + [InlineData(0x4001FF, 0xFFFFFF)] + public void PcToSnes_ValidHeaderHiRom2Address_ReturnsValidPcAddress( + int snesAddress, + int expectedPcAddress) + { + PcToSnes_ValidInput_ReturnsValidAddress( + snesAddress, + true, + AddressMode.HiRom2, + expectedPcAddress); + } + + [Theory] + [InlineData(0x000200, 0x808000)] + public void PcToSnes_ValidHeaderExLoRomAddress_ReturnsValidPcAddress( + int snesAddress, + int expectedPcAddress) + { + PcToSnes_ValidInput_ReturnsValidAddress( + snesAddress, + true, + AddressMode.ExLoRom, + expectedPcAddress); + } + + [Theory] + [InlineData(0x8001FF, 0x3FFFFF)] + public void PcToSnes_ValidHeaderExHiRomAddress_ReturnsValidPcAddress( + int snesAddress, + int expectedPcAddress) + { + PcToSnes_ValidInput_ReturnsValidAddress( + snesAddress, + true, + AddressMode.ExHiRom, + expectedPcAddress); + } + + [Fact] + public void IsValidAddress_InvalidAddressMode_ThrowsInvalidEnumException() + { + static void action() + { + _ = Rom.IsValidAddress(0, (AddressMode)(-1)); + } + + _ = Assert.Throws(action); + } + + [Theory] + [InlineData(Int32.MinValue)] + [InlineData(-1)] + [InlineData(0x000000)] + [InlineData(0x007FFF)] + [InlineData(0x010000)] + [InlineData(0x3F7FFF)] + [InlineData(0x700000)] + [InlineData(0x708000)] + [InlineData(0x770000)] + [InlineData(0x778000)] + [InlineData(0x780000)] + [InlineData(0x788000)] + [InlineData(0x7E0000)] + [InlineData(0x7FFFFF)] + [InlineData(0x800000)] + [InlineData(0xBF7FFF)] + [InlineData(0x1000000)] + [InlineData(0x1008000)] + [InlineData(Int32.MaxValue)] + public void IsValidAddress_InvalidLoRomAddress_ReturnsFalse(int snesAddress) + { + IsValidAddress_InvalidAddress_ReturnsFalse( + snesAddress: snesAddress, + addressMode: AddressMode.LoRom); + } + + [Theory] + [InlineData(0x008000)] + [InlineData(0x3FFFFF)] + [InlineData(0x400000)] + [InlineData(0x6FFFFF)] + //[InlineData(0x808000)] + //[InlineData(0xC00000)] + //[InlineData(0xFF0000)] + //[InlineData(0xFFFFFF)] + public void IsValidAddress_ValidLoRomAddress_ReturnsTrue(int snesAddress) + { + /* + * There are two ways to consider a LOROM address. If I'm trying to read from a LOROM address, then + * it doesn't matter if I get $00:8000 or $80:8000 (also note that even $40:8000 or several other values + * may be valid based on mirroring). They point to the same PC location. However, if I'm trying to write + * to a PC location, then writing in LOROM or LOROM2 actually matters. Not so much because they point to + * different location (because they don't), but because it just affects the written data. + */ + IsValidAddress_ValidAddress_ReturnsTrue( + snesAddress: snesAddress, + addressMode: AddressMode.LoRom); + } + + [Theory] + [InlineData(Int32.MinValue)] + [InlineData(-1)] + [InlineData(0x000000)] + [InlineData(0x007FFF)] + [InlineData(0x010000)] + [InlineData(0x3F7FFF)] + [InlineData(0x700000)] + [InlineData(0x708000)] + [InlineData(0x770000)] + [InlineData(0x778000)] + [InlineData(0x780000)] + [InlineData(0x788000)] + [InlineData(0x7E0000)] + [InlineData(0x7FFFFF)] + [InlineData(0x800000)] + [InlineData(0xBF7FFF)] + [InlineData(0x1000000)] + [InlineData(0x1008000)] + [InlineData(Int32.MaxValue)] + public void IsValidAddress_InvalidLoRom2Address_ReturnsFalse(int snesAddress) + { + IsValidAddress_InvalidAddress_ReturnsFalse( + snesAddress: snesAddress, + addressMode: AddressMode.LoRom2); + } + + [Theory] + [InlineData(0x008000)] + [InlineData(0x3FFFFF)] + [InlineData(0x400000)] + [InlineData(0x6FFFFF)] + [InlineData(0x808000)] + [InlineData(0xC00000)] + [InlineData(0xFF0000)] + [InlineData(0xFFFFFF)] + public void IsValidAddress_ValidLoRom2Address_ReturnsTrue(int snesAddress) + { + IsValidAddress_ValidAddress_ReturnsTrue( + snesAddress: snesAddress, + addressMode: AddressMode.LoRom2); + } + + [Theory] + [InlineData(Int32.MinValue)] + [InlineData(-1)] + [InlineData(0x000000)] + [InlineData(0x007FFF)] + [InlineData(0x010000)] + [InlineData(0x3F7FFF)] + [InlineData(0x700000)] + [InlineData(0x770000)] + [InlineData(0x780000)] + [InlineData(0x7E0000)] + [InlineData(0x7FFFFF)] + [InlineData(0x800000)] + [InlineData(0xBF7FFF)] + [InlineData(0x1000000)] + [InlineData(0x1008000)] + [InlineData(Int32.MaxValue)] + public void IsValidAddress_InvalidExLoRomAddress_ReturnsFalse(int snesAddress) + { + IsValidAddress_InvalidAddress_ReturnsFalse( + snesAddress: snesAddress, + addressMode: AddressMode.ExLoRom); + } + + [Theory] + [InlineData(0x008000)] + [InlineData(0x3FFFFF)] + [InlineData(0x400000)] + [InlineData(0x6FFFFF)] + [InlineData(0x708000)] + [InlineData(0x778000)] + [InlineData(0x788000)] + [InlineData(0x808000)] + [InlineData(0xC00000)] + [InlineData(0xFF0000)] + [InlineData(0xFFFFFF)] + public void IsValidAddress_ValidExLoRomAddress_ReturnsTrue(int snesAddress) + { + IsValidAddress_ValidAddress_ReturnsTrue( + snesAddress: snesAddress, + addressMode: AddressMode.ExLoRom); + } + + [Theory] + [InlineData(Int32.MinValue)] + [InlineData(-1)] + [InlineData(0x000000)] + [InlineData(0x007FFF)] + [InlineData(0x010000)] + [InlineData(0x3F7FFF)] + [InlineData(0x700000)] + [InlineData(0x708000)] + [InlineData(0x770000)] + [InlineData(0x778000)] + [InlineData(0x780000)] + [InlineData(0x788000)] + [InlineData(0x7E0000)] + [InlineData(0x7FFFFF)] + [InlineData(0x800000)] + [InlineData(0xBF7FFF)] + [InlineData(0x1000000)] + [InlineData(0x1008000)] + [InlineData(Int32.MaxValue)] + public void IsValidAddress_InvalidHiRomAddress_ReturnsFalse(int snesAddress) + { + IsValidAddress_InvalidAddress_ReturnsFalse( + snesAddress: snesAddress, + addressMode: AddressMode.HiRom); + } + + [Theory] + [InlineData(0x008000)] + [InlineData(0x3FFFFF)] + [InlineData(0x400000)] + [InlineData(0x6FFFFF)] + [InlineData(0x808000)] + [InlineData(0xC00000)] + [InlineData(0xFF0000)] + [InlineData(0xFFFFFF)] + public void IsValidAddress_ValidHiRomAddress_ReturnsTrue(int snesAddress) + { + IsValidAddress_ValidAddress_ReturnsTrue( + snesAddress: snesAddress, + addressMode: AddressMode.HiRom); + } + + [Theory] + [InlineData(Int32.MinValue)] + [InlineData(-1)] + [InlineData(0x000000)] + [InlineData(0x007FFF)] + [InlineData(0x010000)] + [InlineData(0x3F7FFF)] + [InlineData(0x700000)] + [InlineData(0x708000)] + [InlineData(0x770000)] + [InlineData(0x778000)] + [InlineData(0x780000)] + [InlineData(0x788000)] + [InlineData(0x7E0000)] + [InlineData(0x7FFFFF)] + [InlineData(0x800000)] + [InlineData(0xBF7FFF)] + [InlineData(0x1000000)] + [InlineData(0x1008000)] + [InlineData(Int32.MaxValue)] + public void IsValidAddress_InvalidHiRom2Address_ReturnsFalse(int snesAddress) + { + IsValidAddress_InvalidAddress_ReturnsFalse( + snesAddress: snesAddress, + addressMode: AddressMode.HiRom2); + } + + [Theory] + [InlineData(0x008000)] + [InlineData(0x3FFFFF)] + [InlineData(0x400000)] + [InlineData(0x6FFFFF)] + [InlineData(0x808000)] + [InlineData(0xC00000)] + [InlineData(0xFF0000)] + [InlineData(0xFFFFFF)] + public void IsValidAddress_ValidHiRom2Address_ReturnsTrue(int snesAddress) + { + IsValidAddress_ValidAddress_ReturnsTrue( + snesAddress: snesAddress, + addressMode: AddressMode.HiRom2); + } + + [Theory] + [InlineData(Int32.MinValue)] + [InlineData(-1)] + [InlineData(0x000000)] + [InlineData(0x007FFF)] + [InlineData(0x010000)] + [InlineData(0x3F7FFF)] + [InlineData(0x700000)] + [InlineData(0x770000)] + [InlineData(0x7E0000)] + [InlineData(0x7FFFFF)] + [InlineData(0x800000)] + [InlineData(0xBF7FFF)] + [InlineData(0x1000000)] + [InlineData(0x1008000)] + [InlineData(Int32.MaxValue)] + public void IsValidAddress_InvalidExHiRomAddress_ReturnsFalse(int snesAddress) + { + IsValidAddress_InvalidAddress_ReturnsFalse( + snesAddress: snesAddress, + addressMode: AddressMode.ExHiRom); + } + + [Theory] + [InlineData(0x008000)] + [InlineData(0x3FFFFF)] + [InlineData(0x400000)] + [InlineData(0x6FFFFF)] + [InlineData(0x708000)] + [InlineData(0x778000)] + [InlineData(0x780000)] + [InlineData(0x788000)] + [InlineData(0x808000)] + [InlineData(0xC00000)] + [InlineData(0xFF0000)] + [InlineData(0xFFFFFF)] + public void IsValidAddress_ValidExHiRomAddress_ReturnsTrue(int snesAddress) + { + IsValidAddress_ValidAddress_ReturnsTrue( + snesAddress: snesAddress, + addressMode: AddressMode.ExHiRom); + } + + [Theory] + [InlineData(0x000000, 0x0000, 0x000000)] + [InlineData(0x000000, 0x0001, 0x000001)] + [InlineData(0x00FFFF, 0x0001, 0x000000)] + [InlineData(0x01FFFF, 0xFFFF, 0x01FFFE)] + [InlineData(0xFFFFFF, 0x0001, 0xFF0000)] + [InlineData(0x000000, 0x10000, 0x000000)] + [InlineData(0x000000, -1, 0x00FFFF)] + [InlineData(0x800000, Int32.MaxValue, 0x80FFFF)] + [InlineData(0x7F0000, Int32.MinValue, 0x7F0000)] + public void IncrementSnesAddress_ValidInput_ReturnsValidOutput( + int snesAddress, + int increment, + int expected) + { + var actual = Rom.IncrementSnesAddress( + snesAddress, + increment, + crossBanks: false); + var message = new StringBuilder("SNES address: 0x") + .Append(snesAddress.ToString("X6")).AppendLine() + .Append("Increment: 0x").Append(increment.ToString("X4")).AppendLine() + .Append("Expected: 0x").Append(expected.ToString("X6")).AppendLine() + .Append("Actual: 0x").Append(actual.ToString("X6")).AppendLine() + .ToString(); + + Assert.True(expected == actual, message); + } + + [Theory] + [InlineData("Aladdin (E)")] + [InlineData("Aladdin (F)")] + [InlineData("Aladdin (G) [!]")] + [InlineData("Aladdin (J)")] + [InlineData("Aladdin (U) [!]")] + [InlineData("Arkanoid - Doh It Again (U) [!]")] + [InlineData("Axelay (U)")] + [InlineData("Battletoads & Double Dragon - The Ultimate Team (U) [!]")] + [InlineData("BS Mario Collection 3 (J)")] + [InlineData("Chrono Trigger (U) [!]")] + [InlineData("Contra III - The Alien Wars (U) [!]")] + [InlineData("Contra Spirits (J)")] + [InlineData("Super Probotector - The Alien Rebels (E)")] + [InlineData("Daikaijuu Monogatari II (Japan)")] + [InlineData("Demon's Blazon - Makai-Mura Monshou Hen (J)")] + [InlineData("Demon's Crest (E)")] + [InlineData("Demon's Crest (U) [!]")] + [InlineData("Donkey Kong Country (E) (V1.0) [!]")] + [InlineData("Donkey Kong Country (E) (V1.1) [!]")] + [InlineData("Donkey Kong Country (U) (V1.0) [!]")] + [InlineData("Donkey Kong Country (U) (V1.1)")] + [InlineData("Donkey Kong Country (U) (V1.2) [!]")] + [InlineData("Donkey Kong Country - Competition Cartridge (U)")] + [InlineData("Super Donkey Kong (J) (V1.0)")] + [InlineData("Super Donkey Kong (J) (V1.1)")] + [InlineData("Donkey Kong Country 2 - Diddy's Kong Quest (E) (V1.1) [!]")] + [InlineData("Donkey Kong Country 2 - Diddy's Kong Quest (G) (V1.0)")] + [InlineData("Donkey Kong Country 2 - Diddy's Kong Quest (G) (V1.1) [!]")] + [InlineData("Donkey Kong Country 2 - Diddy's Kong Quest (U) (V1.0)")] + [InlineData("Donkey Kong Country 2 - Diddy's Kong Quest (U) (V1.1) [!]")] + [InlineData("Super Donkey Kong 2 - Dixie & Diddy (J) (V1.0)")] + [InlineData("Super Donkey Kong 2 - Dixie & Diddy (J) (V1.1)")] + [InlineData("Donkey Kong Country 3 - Dixie Kong's Double Trouble (E) [!]")] + [InlineData("Donkey Kong Country 3 - Dixie Kong's Double Trouble (U) [!]")] + [InlineData("Super Donkey Kong 3 - Nazo no Krems Shima (J) (V1.0)")] + [InlineData("Super Donkey Kong 3 - Nazo no Krems Shima (J) (V1.1)")] + [InlineData("Earthbound (U)")] + [InlineData("Mother 2 (J)")] + [InlineData("F-ZERO (E)")] + [InlineData("F-ZERO (J)")] + [InlineData("F-ZERO (U) [!]")] + [InlineData("Final Fantasy - Mystic Quest (U) (V1.0) [!]")] + [InlineData("Final Fantasy - Mystic Quest (U) (V1.1)")] + [InlineData("Final Fantasy USA - Mystic Quest (J)")] + [InlineData("Mystic Quest Legend (E)")] + [InlineData("Mystic Quest Legend (F)")] + [InlineData("Mystic Quest Legend (G)")] + [InlineData("FF4FE.bAAMI_KkGAMAF4kA6JQ.6DYM4S5PAX")] + [InlineData("Final Fantasy II (U) (V1.0) [!]")] + [InlineData("Final Fantasy II (U) (V1.1)")] + [InlineData("Final Fantasy IV (J)")] + [InlineData("Final Fantasy IV - 10th Anniversary Edition v3.21")] + [InlineData("Final Fantasy IV - Ultima v5.12f no header")] + [InlineData("Final Fantasy V (J)")] + [InlineData("Final Fantasy III (U) (V1.0) [!]")] + [InlineData("Final Fantasy III (U) (V1.1) [!]")] + [InlineData("Final Fantasy VI (J)")] + [InlineData("Firepower 2000 (U)")] + [InlineData("Super SWIV (E)")] + [InlineData("Super SWIV (J) (32469)")] + [InlineData("Super SWIV (J) (62746) [b1]")] + [InlineData("Super SWIV (J) (62746) [f1]")] + [InlineData("Super SWIV (J) (62746) [hI]")] + [InlineData("Super SWIV (J) (62746)")] + [InlineData("Gradius III (U) [!]")] + [InlineData("Hagane (Beta)")] + [InlineData("Hagane (E) [o1]")] + [InlineData("Hagane (E)")] + [InlineData("Hagane (J) [b1]")] + [InlineData("Hagane (J) [t1]")] + [InlineData("Hagane (J)")] + [InlineData("Hagane (U) [t1]")] + [InlineData("Hagane (U)")] + [InlineData("Killer Instinct (E) [!]")] + [InlineData("Killer Instinct (U) (V1.0)")] + [InlineData("Killer Instinct (U) (V1.1) [!]")] + [InlineData("King of Dragons, The (J) [f1]")] + [InlineData("King of Dragons, The (J) [t1]")] + [InlineData("King of Dragons, The (J)")] + [InlineData("King of Dragons, The (U) [h1]")] + [InlineData("King of Dragons, The (U) [T+Ita]")] + [InlineData("King of Dragons, The (U)")] + [InlineData("Kirby Super Star (U) [!]")] + [InlineData("Kirby's Avalanche (U) [!]")] + [InlineData("Kirby's Ghost Trap (E)")] + [InlineData("Hoshi no Kirby 3 (J)")] + [InlineData("Kirby's Dream Land 3 (U)")] + [InlineData("Kaizoalttp")] + [InlineData("Legend of Zelda, The - A Link to the Past (E) [!]")] + [InlineData("Legend of Zelda, The - A Link to the Past (F)")] + [InlineData("Legend of Zelda, The - A Link to the Past (FC)")] + [InlineData("Legend of Zelda, The - A Link to the Past (G) [!]")] + [InlineData("Legend of Zelda, The - A Link to the Past (U) [!]-rand-pal")] + [InlineData("Legend of Zelda, The - A Link to the Past (U) [!]")] + [InlineData("Zelda no Densetsu - Kamigami no Triforce (J) (V1.0) [b1]")] + [InlineData("Zelda no Densetsu - Kamigami no Triforce (J) (V1.0)")] + [InlineData("Zelda no Densetsu - Kamigami no Triforce (J) (V1.1)")] + [InlineData("Zelda no Densetsu - Kamigami no Triforce (J) (V1.2)")] + [InlineData("Zelda Parallel Worlds")] + [InlineData("Magical Quest Starring Mickey Mouse, The (U) [!]")] + [InlineData("Marko's Magic Football (E) [f1]")] + [InlineData("Marko's Magic Football (E)")] + [InlineData("Mega Man X (E)")] + [InlineData("Mega Man X (U) (V1.0) [!]")] + [InlineData("Mega Man X (U) (V1.1)")] + [InlineData("Rockman X (J) (V1.0) [!]")] + [InlineData("Rockman X (J) (V1.1)")] + [InlineData("Ms. Pac-Man (U)")] + [InlineData("Pilotwings (E) [!]")] + [InlineData("Pilotwings (J)")] + [InlineData("Pilotwings (U) [!]")] + [InlineData("Pilotwings (U) [f1]")] + [InlineData("Power Lode Runner (J) (NP)")] + [InlineData("Run Saber (Beta) [h1]")] + [InlineData("Run Saber (Beta)")] + [InlineData("Run Saber (E) [!]")] + [InlineData("Run Saber (U) [!]")] + [InlineData("Run Saber (U) [t1]")] + [InlineData("Run Saber (U) [t2]")] + [InlineData("Run Saber (U) [t3]")] + [InlineData("Secret of Mana (E) (V1.0)")] + [InlineData("Secret of Mana (E) (V1.1) [!]")] + [InlineData("Secret of Mana (F)")] + [InlineData("Secret of Mana (G)")] + [InlineData("Secret of Mana (U) [!]")] + [InlineData("Seiken Densetsu 2 (J)")] + [InlineData("Shin Megami Tensei (J) (V1.0)")] + [InlineData("Shin Megami Tensei (J) (V1.1)")] + [InlineData("Space Megaforce (U) [!]")] + [InlineData("Space Megaforce (U) [T+Spa100%_Sayans]")] + [InlineData("Super Aleste (E) [h1C]")] + [InlineData("Super Aleste (E)")] + [InlineData("Super Aleste (J) [t1]")] + [InlineData("Super Aleste (J) [t2]")] + [InlineData("Super Aleste (J)")] + [InlineData("Star Fox (J) [!]")] + [InlineData("Star Fox (U) (V1.0) [!]")] + [InlineData("Star Fox (U) (V1.2) [!]")] + [InlineData("StarWing (E) (V1.0) [!]")] + [InlineData("StarWing (E) (V1.1) [!]")] + [InlineData("StarWing (G) [!]")] + [InlineData("StarWing Offizieller Wettbewerb (G) [!]")] + [InlineData("StarWing Super Weekend Competition (E) [!]")] + [InlineData("Stunt Race FX (E) [!]")] + [InlineData("Stunt Race FX (U) [!]")] + [InlineData("Wild Trax (J)")] + [InlineData("Akumajou Dracula (J) [!]")] + [InlineData("Super Castlevania IV (E) [!]")] + [InlineData("Super Castlevania IV (U) [!]")] + [InlineData("Super Mario 3 Expert (Hack)")] + [InlineData("Super Mario All-Stars (E) [!]")] + [InlineData("Super Mario All-Stars (U) [!] - 8MB")] + [InlineData("Super Mario All-Stars (U) [!]")] + [InlineData("Super Mario All-Stars + Super Mario World (E) [!]")] + [InlineData("Super Mario All-Stars + Super Mario World (U) [!]")] + [InlineData("Super Mario Bros. (U)")] + [InlineData("Super Mario Collection (J) (V1.0)")] + [InlineData("Super Mario Collection (J) (V1.1)")] + [InlineData("Test ROM")] + [InlineData("Mario Kart R")] + [InlineData("Super Mario Kart (E) [!]")] + [InlineData("Super Mario Kart (J)")] + [InlineData("Super Mario Kart (U) [!]")] + [InlineData("SMRPG_GBARP_US_7.1.8_full_4121810938")] + [InlineData("Super Mario RPG - Legend of the Seven Stars (U) [!]")] + [InlineData("Super Mario RPG Armageddon v10 (Hard)")] + [InlineData("Super Mario RPG Armageddon v10 (Normal)")] + [InlineData("Bowser's Strike Back")] + [InlineData("Brutal Mario")] + [InlineData("Demo World - The Legend Continues")] + [InlineData("Kaizo Kindergarten v1.09")] + [InlineData("Luigi's Adventure")] + [InlineData("Mario Is Missing 2")] + [InlineData("Mario's Return")] + [InlineData("MarioX World Chronicles")] + [InlineData("Panic In the Mushroom Kingdom")] + [InlineData("Return to Dinosaur Land")] + [InlineData("SMW - HELL Edition")] + [InlineData("Super Mario Islands")] + [InlineData("Super Mario World (U) [!] - 8MB Hack")] + [InlineData("Super Mario World (U) [!]")] + [InlineData("Super Mario World R 2")] + [InlineData("Super Mario World R EX")] + [InlineData("Super Mario World R")] + [InlineData("The Adventures of Phantasm World")] + [InlineData("The Second Reality Project (Hard Type)")] + [InlineData("The Second Reality Project (SNES Type)")] + [InlineData("VLDC11_BaseROM")] + [InlineData("Super Mario - Yoshi Island (J) (V1.0)")] + [InlineData("Super Mario - Yoshi Island (J) (V1.1) [!]")] + [InlineData("Super Mario - Yoshi Island (J) (V1.2)")] + [InlineData("Super Mario World 2 - Yoshi's Island (E) (V1.0) [!]")] + [InlineData("Super Mario World 2 - Yoshi's Island (E) (V1.1)")] + [InlineData("Super Mario World 2 - Yoshi's Island (U) (V1.0) [!]")] + [InlineData("Super Mario World 2 - Yoshi's Island (U) (V1.1)")] + [InlineData("Super MarioWorld 2+")] + [InlineData("Super Metroid (E) [!]")] + [InlineData("Super Metroid (JU) [!]")] + [InlineData("Tales of Phantasia (J) [!]")] + [InlineData("Tetris & Dr. Mario (E) [!]")] + [InlineData("Tetris & Dr. Mario (U) [!]")] + [InlineData("BS Panel de Pon Event '98 (J)")] + [InlineData("BS Yoshi no Panepon (J)")] + [InlineData("Panel de Pon (J)")] + [InlineData("Tetris Attack (E)")] + [InlineData("Tetris Attack (U) [!]")] + [InlineData("Tetris Attack (U) [f1]")] + [InlineData("Tetris Attack (U) [T+Spa050_A2j]")] + [InlineData("Uniracers (U) [!]")] + [InlineData("Unirally (E) [!]")] + + public void Constructor_ValidData_CanDetermineFormat(string name) + { + var testRom = TestRoms[name]; + var data = testRom.CreateRom(); + Rom rom; + try + { + rom = new Rom(data); + } + catch (Exception ex) + { + var exceptionMessage = new StringBuilder(name) + .AppendLine(" could not get address mode.") + .Append(ex.Message).AppendLine() + .ToString(); + Assert.True(false, exceptionMessage); + return; + } + + var message = new StringBuilder("File Name: ").Append(name) + .AppendLine() + .Append("Expected Format: ").Append(testRom.AddressMode).AppendLine() + .Append("Actual Format: ").Append(rom.AddressMode).AppendLine() + .ToString(); + var expectedAddressMode = Enum.Parse(testRom.AddressMode!); + var actualAddressMode = rom.AddressMode; + if (actualAddressMode == AddressMode.LoRom2) + { + actualAddressMode = AddressMode.LoRom; + } + + if (actualAddressMode == AddressMode.HiRom2) + { + actualAddressMode = AddressMode.HiRom; + } + + Assert.True( + condition: expectedAddressMode == actualAddressMode, + userMessage: message); + } + + private static void SnesToPc_ValidInput_ReturnsValidAddress( + int snesAddress, + bool hasHeader, + AddressMode addressMode, + int expectedPcAddress) + { + var actualAddress = Rom.SnesToPc(snesAddress, hasHeader, addressMode); + var message = new StringBuilder() + .Append("Address Mode: ").Append(addressMode).AppendLine() + .Append("Has Header: ").Append(hasHeader).AppendLine() + .Append("SNES Address: 0x").Append(snesAddress.ToString("X6")).AppendLine() + .Append("Expected PC Address: 0x") + .Append(expectedPcAddress.ToString("X6")).AppendLine() + .Append("Actual PC Address: 0x").Append(actualAddress.ToString("X6")) + .AppendLine() + .ToString(); + + Assert.True( + condition: expectedPcAddress == actualAddress, + userMessage: message); + } + + private static void PcToSnes_ValidInput_ReturnsValidAddress( + int pointer, + bool hasHeader, + AddressMode addressMode, + int expectedAddress) + { + var actualAddress = Rom.PcToSnes(pointer, hasHeader, addressMode); + var message = new StringBuilder() + .Append("Address Mode: ").Append(addressMode).AppendLine() + .Append("Has Header: ").Append(hasHeader).AppendLine() + .Append("PC Address: 0x").Append(pointer.ToString("X6")).AppendLine() + .Append("Expected SNES Address: 0x") + .Append(expectedAddress.ToString("X6")).AppendLine() + .Append("Actual SNES Address: 0x").Append(actualAddress.ToString("X6")) + .AppendLine() + .ToString(); + + Assert.True( + condition: expectedAddress == actualAddress, + userMessage: message); + } + + private static void IsValidAddress_ValidAddress_ReturnsTrue( + int snesAddress, + AddressMode addressMode) + { + var result = Rom.IsValidAddress(snesAddress, addressMode); + var message = new StringBuilder("Address Mode: ").Append(addressMode) + .AppendLine() + .Append("SNES Address: 0x").Append(snesAddress.ToString("X6")) + .AppendLine() + .ToString(); + + Assert.True( + condition: result, + userMessage: message); + } + + private static void IsValidAddress_InvalidAddress_ReturnsFalse( + int snesAddress, + AddressMode addressMode) + { + var result = Rom.IsValidAddress(snesAddress, addressMode); + var message = new StringBuilder("Address Mode: ").Append(addressMode) + .AppendLine() + .Append("SNES Address: 0x").Append(snesAddress.ToString("X6")) + .AppendLine() + .ToString(); + + Assert.False( + condition: result, + userMessage: message); + } +} diff --git a/test/Snes.Tests/Snes.Tests.csproj b/test/Snes.Tests/Snes.Tests.csproj new file mode 100644 index 0000000..fee6b91 --- /dev/null +++ b/test/Snes.Tests/Snes.Tests.csproj @@ -0,0 +1,43 @@ + + + + net8.0 + enable + enable + + false + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + True + True + Resources.resx + + + + + + ResXFileCodeGenerator + Resources.Designer.cs + + + + diff --git a/test/Snes.Tests/TestRomGenerator.cs b/test/Snes.Tests/TestRomGenerator.cs new file mode 100644 index 0000000..324e655 --- /dev/null +++ b/test/Snes.Tests/TestRomGenerator.cs @@ -0,0 +1,61 @@ +namespace Maseya.Snes.Tests; + +using System; + +/// +/// Generates test ROM data using member configuration values. +/// +/// +/// The member will be serialized through JSON files. +/// +internal class TestRomGenerator +{ + public string? AddressMode { get; set; } + + public int Size { get; set; } + + public string? LoRom { get; set; } + + public string? HiRom { get; set; } + + public string? ExLoRom { get; set; } + + public string? ExHiRom { get; set; } + + public byte[] CreateRom() + { + var result = new byte[Size]; + result[0] = 0x78; + result[1] = 0x9C; + for (var i = 0; i < 0x200; i++) + { + result[i] = 0xFF; + } + + if (!String.IsNullOrEmpty(LoRom)) + { + var loRom = Convert.FromBase64String(LoRom); + loRom.CopyTo(result, 0x7FB0); + } + + if (!String.IsNullOrEmpty(HiRom)) + { + var hiRom = Convert.FromBase64String(HiRom); + hiRom.CopyTo(result, 0xFFB0); + } + + if (!String.IsNullOrEmpty(ExLoRom)) + { + var exLoRom = Convert.FromBase64String(ExLoRom); + exLoRom.CopyTo(result, 0x407FB0); + } + + if (!String.IsNullOrEmpty(ExHiRom)) + { + var exHiRom = Convert.FromBase64String(ExHiRom); + exHiRom.CopyTo(result, 0x40FFB0); + } + + return result; + } +}