Skip to content

Developer Guide

ShinyTillDawn edited this page Jun 5, 2024 · 10 revisions

Overview

HexManiacAdvance (intenally known as just HexManiac) is designed as four main parts.

  • HexManiac.Core: Contains the meat of the logic.
  • HexManiac.Integration: Contains 30+ automated tests, validating complicated features that need real data to work.
  • HexManiac.Tests: Contains 2500+ unit tests, validating that all the individual features work as expected.
  • HexManiac.WPF: Contains all the UI elements, controlling how everything is displayed.

The application is built using the Model-View-ViewModel design pattern. All the View pieces are contained within HexManiac.WPF, while all the ViewModel and Model parts are stored in HexManiac.Core.

The Model

Primary class to look at: src/HexManiac.Core/Models/PokemonModel.cs

The model for a single file is broken between two categories of types. Runs, which represent ranges of related data: and DataFormats, which represent the metadata for a single byte. All the runs for a file exist all the time, but DataFormats are added and removed on the fly based on what the user is doing. Besides the list of Runs, the Model is also in charge of tracking other metadata, such as

  • Constants (Addresses that all contain the same logical value, so they need to be updated in sync)
  • Lists (Enumerations of names for things that don't have names stored in the data)

Runs

Once a file is loaded into a byte array, the main purpose of the model is to add metadata, making it possible to understand the purpose behind different sections of the file. To this end, the model primarily stores an ordered list of 'runs' (see IFormattedRun) which obey the following rules:

  • A run has a start point and a length.
  • Runs cannot overlap.
  • Runs know about locations that point to them.
  • Runs know how to create a "data format" (see IDataFormat) that describes a single byte within the run.

Example of a Run

The highlighted bytes are a text run. The run includes the start point, the length, and the format. The actual data isn't considered part of the run object.

In general, this system is fairly lightweight and flexible, allowing a run to describe a large number of different things within the file. For example, PCSRun describes a stream of bytes that represents text within the game. The run contains some utility methods for working with that data, but the entire data structure is only 16 bytes larger than a default object.

DataFormats

Example Data Format

The highlighted byte is displayed using a DataFormat. A run creates a separate DataFormat for each byte. DataFormats are created on demand, and only exist for data currently being viewed.

As mentioned, each run knows how to create a 'data format' (see IDataFormat) for a specific byte. This smaller model object contains only a single method by default: Visit. IDataFormat uses the visitor design pattern. As a downside, this means that IDataFormat and IDataFormatVisitor need to be aware of all implementations of IDataFormat. As an upside, this means that IDataFormat need not be aware of any operations that can be performed differently depending on which data format is in use. This is crucial, because many, many parts of the code need to behave differently depending on the data format, many of which the model should not know about. Some examples include:

  • Working with Cut/Copy/Paste/Delete (how to serialize/deserialize the data).
  • Working with keyboard edits (what characters are valid, auto-complete, etc).
  • Bringing up different context menus when the user right-clicks.
  • Drawing a cell in the user interface containing the formatted byte.

This last interaction specifically benefits from the Visitor pattern by allowing "how to draw the cell" to depend purely on a cell's data format, even though the implementation must live in the View.

Thanks to the visitor pattern, each of these operations need to know about every possible DataFormat, but none of them need to know about each other. This allows code to be logically grouped to allow for a new operation or change to an operation to touch only a single file, while adding a new data format provides useful compile-time information to make sure no interactions get forgotten by the programmer.

The ViewModel

Primary class to look at: src/HexManiac.Core/ViewModels/ViewPort.cs

While the model mostly deals with an entire file, the ViewModel deals with only the part of a file that a user can see at a given time: a 'view port'. Within a single tab, the ViewPort is the most important ViewModel object, supported by the ToolTray. The tabs are collected together into the EditorViewModel, which represents the overall editor wrapped around the tabs, containing extra features like undo/redo, open new file, and save all. The EditorViewModel can contain other kinds of tabs, such as the pokedex order editor, image editor, or map editor. Each has their own desires for what to display, so each has their own ViewModel.

EditorViewModel image

The EditorViewModel is the ViewModel for the entire window: it handles multiple tabs and the menu.

ViewPort

The ViewPort includes the tools and hex associated with a single tab / single file.

ViewPort helpers: ToolTray, ScrollRegion, and Selection

The basic feature of the ViewPort is to provide an area (width, height, and data offset) in which to present data. This general "what am I showing" behavior is handled by the ScrollRegion.

Beyond that, the ViewPort needs to handle the cursor and data selection. The keyboard and mouse commands for this live in the Selection helper class.

In addition, the ViewPort has a set of tools for editing data in a more user-friendly way. These tools are owned by the ToolTray helper class.

Next, the ViewPort is in change of editing. The ViewPort handles editing directly, although many operations are offloaded to visitors for the different data formats, since editing often depends on the data format.

ViewPort helpers

The ViewPort relies on the ToolTray (in blue), the Selection (in green), and the ScrollRegion (in red) classes to help manage the various ideas it's in charge of.

The View

The last assembly, HexManiac.WPF, contains not only all of the UI, but all of the IO. The class WindowsFileSystem in in charge of abstracting away the file system and clipboard, and App.xaml.cs handles command line arguments. But other than a few loose ends like that, you can think of the View as containing four layers.

  • The MainWindow includes the menu bar, start screen, and tabs.
  • A TabView shows the controls for a single file, including the tools, scrollbars, status bar, and hex area.
  • The HexContent maps to what's currently in the display of a single ViewPort ViewModel object, handling display and mouse/keyboard interaction.
  • The FormatDrawer contains the logic for how to draw single cells of content on the screen.

MainWindow

The Menu

Along with the always visible menu bar, the MainWindow is in charge of four context-sensitive controls that are displayed in the same space: Goto, Find, Messages, and Errors. All of these are shared by all of the tabs, so the MainWindow uses the EditorViewModel to control these.

To improve visibility of these extra context-sensitive controls, the MainWindow also has a FocusAnimationElement, who's sole purpose is to pull the users focus up to the upper right corner whenever one of these context-sensitive controls appears.

The Tabs

When there are no tabs showing, the MainWindow uses StartScreen to display useful (hopefully) information. This serves to keep the screen from being totally blank and help users get some ideas for getting started, as well as hopefully educating new users on the most important concepts for working with HexManiac: Pointers and Anchors.

Custom Code

Not all of the logic is handled by the ViewModels. Whenever the user clicks a button that is routed directly to another IO task (such as opening the Wiki, opening the theme editor, or drag-dropping a file), the MainWindow includes custom code (which may call into the ViewModel). This code should be kept to a minimum, since code in the View cannot easily be tested with the automated tests.

TabView

TabView

Parts of the TabView. The tools (in blue), the anchor editor (in red), and the row headers (in yellow). The column headers (in orange) are rendered by the HorizontalSlantedTextControl helper class.

The Tools

HexManiac provides custom tools to help work with some of the data. For example, text can be easily displayed in the hex view by just displaying text characters instead of raw hex bytes, but editing text in this way can be a bit of a chore. So a Text Tool is provided that lets you edit text in a more conventional way. The markup of the various tools lives in the TabView.xaml.

  • Text Tool: lets the user edit text-like data in a textbox instead of as individually overwritten cells.
  • Table Tool: lets the user edit single rows of table data with fields, combo boxes, and check boxes. Displays the data in a format that is less space-constrained, so it's easier to see the labels of each data part.
  • Code Tool: lets the user inspect the raw bytes based on their own knowledge instead of relying on data formatting. This can let them read assembly code, in-game scripts, or just look at raw bytes instead of formatted data to improve their understanding of what's going on in the file.
  • Image Tool: lets the user see various sprites (tilemaps, compressed, uncompressed, etc.) that HMA recognized from raw data. For some sprites, you can change how it looks by scrolling through detected palettes. The tool also allows you to import & edit images without worrying about byte changes as well as exporting images to your computer.

Anchor Editor

Since anchors aren't really displayed in the data due to limited space, the TabView includes an Anchor Editor that lets you view and modify anchors that are already within the data. This can let you quickly copy/paste anchors and adjust formats, or just let you quickly see the name of a section you're looking at.

Row and Column Headers

Generally, these display the address of the line and the offset of the current column, letting you scan the data quickly. However, this can be configured by the ViewModel, allowing it to show useful labels for rows and columns when names are more useful. Note that the column headers are displayed using a custom FrameworkElement called HorizontalSlantedTextControl, because displaying overlapping horizontal text in WPF is... complicated enough to warrant its own class.

HexContent

The majority of the TabView is filled with a single giant custom element, the HexContent. The HexContent`s job is to feel like a giant grid filled with cells, but to implement this very efficiently. This means that instead of using FrameworkElements for each cell, the entire grid is just a single flat FrameworkElement that manually handles mouse, keyboard, and resize events.

Based on its relative lack of visual depth, HexContent doesn't have a xaml file: everything is handled in code. A lot of that code simply forwards to the ViewPort to perform work, but any pixel coordinates or keyboard events are first translated into a system-agnostic form. Likewise, the HexContent is responsible for displaying context menus, but it does so based on IContextItem objects fetched from its ViewPort.

The code in HexContent is focused entirely on this translation or the creation/placement of visual elements that are displayed along with the grid, such as the autocomplete menu. The actual code for rendering individual cells goes one level further.

FormatDrawer

As mentioned earlier, the ViewPort contains a Width and Height, and any cell within that space can be queried for its data and its format. The formats use the Visitor Pattern to allow arbitrary operations to be run for all the formats. The FormatDrawer is one such operation: given a format, it understands how to draw it. The HexContent calls the FormatDrawer for every cell.

For performance reasons, the FormatDrawer uses a combination of FormattedTexts (for special cases) and GlyphRuns (for bulk drawing). Working with GlyphRun is painful, so most is abstracted away into the GlyphCollector helper class. Using FormattedText makes the code easy to work with, but a little slow. Using GlyphRun makes it possible to draw entire rows in a single sweep, massively speeding up the rendering.

FormatDrawer

The FormatDrawer asks each cell for a DataFormat, and renders the cell differently depending on the DataFormat. It can render the cell as text, a number, an enum, a pointer, or many other things. The HexContent then adds extras like the cell-grid borders, the selection border, and horizontal scrolling as needed. The HexContent is in charge of all mouse and keyboard input, but lets the FormatDrawer do most of the conditional drawing logic.

Clone this wiki locally