Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds support for per scene and project wide DMG palettes #1601

Open
wants to merge 24 commits into
base: develop
Choose a base branch
from

Conversation

Q-Bert-Reynolds
Copy link
Contributor

@Q-Bert-Reynolds Q-Bert-Reynolds commented Oct 3, 2024

  • Please check if the PR fulfills these requirements
  • The commit message follows our guidelines
  • Tests for the changes have been added (for bug fixes / features)
  • Docs have been added / updated (for bug fixes / features)
  • What kind of change does this PR introduce? (Bug fix, feature, docs update, ...)

I guess this is more of a feature than a bug fix. Builds had an unexpected behavior, so I changed the editor to leverage that behavior instead of changing the engine to get rid of it.

  • What is the current behavior? (You can also link to an open issue here)

When you play a build on a DMG, palettes are reset on scene load. You can set BGP, OBP0, and OBP1 via GBVM or engine eject, but your changes are overwritten when you load the next scene. I thought this was an odd behavior, so I went digging and found out that monochrome palette data is written per scene at build time to a fixed value, and those values can't be configured. This is especially problematic for scenes where you want the player to go behind objects, but you also want black to be the color the player is in front of. I could easily eject the engine and prevent the palette loading, but it was painful looking at inverted colors in the editor.

  • What is the new behavior (if this is a feature change)?

This PR introduces a series of quality of life improvements for the DMG and GBP. It allows you to select project wide or per scene monochrome palettes (ie. BGP, OBP0, and OBP1). Your selected monochrome palettes are reflected in the scene, background, and sprite views when in mono mode or when previewing as mono. To that end, I've also added the option to preview as mono in the background and sprite views.

  • Does this PR introduce a breaking change? (What changes might users need to make in their application due to this PR?)

Nope. It doesn't touch the engine code at all. It only added 3 optional fields to scene save data (no migration needed). Most of the editor code was left untouched. Probably the most radical changes were to ColorizedImage.

  • Other information:

I've never used TypeScript or React or Electron. I have no idea what I'm doing. I would love to know if there are better ways of going about some of this stuff - particularly with forms and styling.

In the future, I'd like to make the default monochrome colors configurable and add Super Game Boy preview options, but I decided those were beyond the scope of this PR.

Here's some screenshots:
{B8FDAB4D-BDED-4092-9504-A0F181A881DF}
{F8518788-23E6-4153-99D5-644DC49BCFFC}
{5801F881-ED0D-4C58-AC73-A74A1309CBD9}
{A50CB336-740C-427C-9245-EE63B41983F8}

Comment on lines 65 to 69
export const indexSpriteColour = (g: number, objPalette: ObjPalette) => {
if (g < 65) {
return 3;
}
if (g < 130) {
return objPalette === "OBP1" ? 2 : 3;
}
if (g < 205) {
return objPalette === "OBP1" ? 2 : 1;
}
return 0;
//NOTE: this assumes GB Studio's sprite image requirements are met... https://www.gbstudio.dev/docs/assets/sprites#image-requirements
export const indexSpriteColour = (g: number, objPalette: [number, number, number, number]) => {
if (g < 130) return objPalette[3];
if (g < 205) return objPalette[2];
return objPalette[1];
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This also fixes a bug. Because "white" is always used as transparency, only 3 colors can actually be mapped. The previous version of this function only returned 3 different numbers (either 0,1,3 or 0,2,3), but it did so with 4 different inputs. This resulted in editor previews of sprites set to OBP1 that were different than what you would see in builds. Of course, you could only see this preview in the sprite sheet select in the game world view, so I don't suppose most folks would have noticed.

Comment on lines +70 to +75
const value = useAppSelector((state) => {
if (state.project.present.settings.previewAsMono) return "0";
const sceneId = state.editor.previewAsSceneId;
if (sceneId === "") return "1";
return sceneId;
});
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added a mono mode, similar to the one found on the world page, to the "preview as" drop down. Previously, only one non-scene option was available in this drop down, "Default Colors", which was used when previewAsSceneId was "". To facilitate more options, I'm just using the stringified index of the non-scene values. To ensure a consistent experience when switching between pages, I set the default state to "0" when previewAsMono and "1" when previewAsSceneId is unset.

Comment on lines 152 to 159
const onChange = (newValue: string) => {
dispatch(
settingsActions.editSettings({
previewAsMono: (newValue == "0"),
})
);
dispatch(editorActions.setPreviewAsSceneId(newValue));
};
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm also using the "preview as" drop down selection to drive the previewAsMono editor setting so if you select it here or in the sprites page, the scenes will be in monochrome when you return to the world page.

@@ -190,7 +201,8 @@ const BackgroundPreviewSettings = ({
? l10n("FIELD_PREVIEW_AS_SCENE", {
sceneName: sceneName(scene, sceneIndex),
})
: l10n("FIELD_PREVIEW_AS_DEFAULT")}
: ([l10n("FIELD_PREVIEW_AS_MONO"), l10n("FIELD_PREVIEW_AS_DEFAULT")][+value])
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As someone who has never used TypeScript before, I found the [+value] syntax a little strange, but it's what StackOverflow told me to do. I'm considering revisiting the weird stringified index hack when I add SGB preview support anyway, but this is a little too hacky for your tastes now, let me know.

Comment on lines -742 to -749
{colorsEnabled && (
<SidebarColumn>
<FormRow>
<FormField
name="playerSpriteSheetId"
label={
<>
{l10n("FIELD_SCENE_BACKGROUND_PALETTES")}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now that there's a palette to edit even in monochrome mode, I moved the colorsEnabled check after the label. I had a strong urge to update the indentation everywhere, but I left it alone to avoid a massive unreadable diff. There are a few bits that looked too odd without any indentation changes, but it's probably not a bad idea to fully correct the indentation levels for consistency after the bulk of the code review is done.

@@ -816,6 +853,25 @@ export const SceneEditor = ({ id }: SceneEditorProps) => {
name="playerSpriteSheetId"
label={l10n("FIELD_SCENE_SPRITE_PALETTES")}
>
{monoEnabled &&
<PaletteButtons>
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps PaletteButtons was the wrong wrapper node to use here, but I used it as a quick and dirty way of keeping the OBP0 and OBP1 palette pickers on the same row.


const settings = useAppSelector((state) => state.project.present.settings);
const dmgColors = [settings.customColorsWhite, settings.customColorsLight, settings.customColorsDark, settings.customColorsBlack];
const fields = isSpritePalette ? [1, 2, 3] : [0, 1, 2, 3];
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At one point, I was showing all 4 input boxes for sprites. Then, I realized the lowest two bits are never used anyway and it felt silly to show them. I didn't want to change the underlying data structure from [number, number, number, number] to number[] though because dealing with arrays of different sizes complicated the logic further and didn't line up as well with the byte that it represented in a build.

Comment on lines +27 to +40
const width = showName ? `max-width: ${isSpritePalette ? 121 : 245}px;` : "width: 100%";
const DMGPalettePickerStyle = styled.div`
display: flex;
${width}
box-sizing: border-box;
`

const DMGPaletteTextStyle = styled.div`
width: 9px;
color: grey;
font-size: 9px;
text-align: center;
transform-origin:50% 50%;
transform: rotate(90deg) translate(-7px,8px);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a lot of hacky stuff going on with the styling:

  • I wanted the pickers to be full width in the settings screen, but only as wide as the color palette buttons and for OBP0 and OBP1 to be on the same row in the scene editor.
  • I wanted there to be text labels in the scene editor, but I wanted them to be tiny, greyed out, unobtrusive, and vertical.
  • I killed off the spacing between numerical inputs so the elements look more like they belong to each other, but it makes the red selection outline look a little funky since it expects a 5px spacing.
  • In the getRows function below, I explicitly set the background and text colors from the default color palette. Didn't know of a cleaner way of doing that. Also, the text color seems fine, but using an offset index to lookup the palette color may be problematic in the future if the default DMG palette is ever configurable.
  • I also changed the min size of NumberInput to allow both sprite palettes to fit on a single row and only be as wide as the palette buttons below it. I didn't notice any issues, but if there's a way to override the styling of child elements instead of just changing the min-width globally, I'd rather do that.

const settings = useAppSelector((state) => state.project.present.settings);
const obp0 = settings.defaultOBP0;
const obp1 = settings.defaultOBP1;
const dmgPal = [settings.customColorsWhite, settings.customColorsLight, settings.customColorsDark, settings.customColorsBlack];
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I used the custom colors from settings instead of DMG_PALETTE.colors because I thought for sure I'd be making those configurable in this PR. I still plan on doing that, but I realized how cumbersome the task was when the various workers complained about trying to access settings off the main thread, so I abandoned ship there.

...selectProps
}) => {
const scenes = useAppSelector((state) => sceneSelectors.selectAll(state));
const optionalScenes = (optionalLabels || []).map((optionalLabel, i) => ({ value: i.toString(), label: optionalLabel }) ) as SceneOption[];
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When there was only one non-scene option, a SceneOption was created with a value of "". To facilitate more non-scene options, I use their index as the value. It's pretty hacky, but it's simple, easy to work with, and required minimal changes.

min-width: 45px;
min-width: 10px;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did this to allow for a smaller DMGPalettePicker, and I couldn't figure out a way to override the style. I don't think it causes problems, but I need to do some more digging/testing.

@chrismaltby
Copy link
Owner

Hi @Q-Bert-Reynolds (great username btw) just getting over a bit of illness so I've been away for a while but looks like you've been pretty busy on this. From a real quick glance through the code seems reasonable, maybe a few things I'd tidy up and appreciate all the comments you've added in the PR will definitely help me reviewing this.

Just in the middle of a quite big change myself (adds a Plugin Manager + a few new plugin types) so I'll want to get that merged before reviewing/merging this but from what I see we shouldn't have any conflicts. When I get to this will let you know if I have any questions. Thanks.

@Q-Bert-Reynolds Q-Bert-Reynolds marked this pull request as ready for review October 14, 2024 23:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants