Skip to content

Commit

Permalink
True 24bit dos color; palette editing (#21)
Browse files Browse the repository at this point in the history
* reworked nfs1 bitmap 8 bit transparency

* fixed 24bit dos color transform

* fixed GEO parts positions in exported mesh

* simple palette editor
  • Loading branch information
AndyGura authored Apr 18, 2024
1 parent 7d6a11b commit 41ae979
Show file tree
Hide file tree
Showing 12 changed files with 114 additions and 51 deletions.
3 changes: 3 additions & 0 deletions frontend/dist/gui/3rdpartylicenses.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
@angular-material-components/color-picker
MIT

@angular/animations
MIT

Expand Down
2 changes: 1 addition & 1 deletion frontend/dist/gui/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,6 @@
<style>.mat-typography{font-size:14px;font-weight:400;line-height:20px;font-family:Roboto,sans-serif;letter-spacing:.0178571429em}html,body{height:100%}body{margin:0;font-family:Roboto,Helvetica Neue,sans-serif}</style><link rel="stylesheet" href="styles.9650df2468906825.css" media="print" onload="this.media='all'"><noscript><link rel="stylesheet" href="styles.9650df2468906825.css"></noscript></head>
<body class="mat-typography">
<app-root></app-root>
<script src="runtime.1713a2863c880bc2.js" type="module"></script><script src="polyfills.6ea95c2235df2b00.js" type="module"></script><script src="scripts.d5dc2919fc5b98cb.js" defer></script><script src="main.016624a0831a82b7.js" type="module"></script>
<script src="runtime.1713a2863c880bc2.js" type="module"></script><script src="polyfills.6ea95c2235df2b00.js" type="module"></script><script src="scripts.d5dc2919fc5b98cb.js" defer></script><script src="main.84537cb5a012b3bb.js" type="module"></script>

</body></html>
1 change: 1 addition & 0 deletions frontend/dist/gui/main.84537cb5a012b3bb.js

Large diffs are not rendered by default.

17 changes: 17 additions & 0 deletions frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
},
"private": true,
"dependencies": {
"@angular-material-components/color-picker": "^15.0.0",
"@angular/animations": "^15.0.0",
"@angular/cdk": "^15.0.0",
"@angular/common": "^15.0.0",
Expand Down
12 changes: 11 additions & 1 deletion frontend/src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,11 @@ import { EacsAudioBlockUiComponent } from './components/editor/eac/eacs-audio.bl
import { GeoGeometryBlockUiComponent } from './components/editor/eac/geo-geometry.block-ui/geo-geometry.block-ui.component';
import { ObjViewerComponent } from './components/editor/common/obj-viewer/obj-viewer.component';
import { BaseArchiveBlockUiComponent } from './components/editor/eac/base-archive.block-ui/base-archive.block-ui.component';
import {
MAT_COLOR_FORMATS,
NGX_MAT_COLOR_FORMATS,
NgxMatColorPickerModule,
} from '@angular-material-components/color-picker';

@NgModule({
declarations: [
Expand Down Expand Up @@ -107,8 +112,13 @@ import { BaseArchiveBlockUiComponent } from './components/editor/eac/base-archiv
MatMenuModule,
ReactiveFormsModule,
MatOptionModule,
NgxMatColorPickerModule,
],
providers: [
EelDelegateService,
NgxDeepEqualsPureService,
{ provide: MAT_COLOR_FORMATS, useValue: NGX_MAT_COLOR_FORMATS },
],
providers: [EelDelegateService, NgxDeepEqualsPureService],
bootstrap: [AppComponent],
})
export class AppModule {}
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,8 @@
<div id="colors-container">
<div *ngFor="let color of resourceData?.colors; let i = index" class="color-box"
[ngStyle]="{'background-color': '#' + lpad(color.toString(16), '0', 8) }"
[matTooltip]="i + ': #' + color.toString(16)"></div>
[matTooltip]="i + ': #' + color.toString(16)"
(click)="onColorClicked($any($event.currentTarget!), i)"></div>
</div>
<input class="hidden" matInput [ngxMatColorPicker]="picker" (colorChange)="onColorChange($event.value)">
<ngx-mat-color-picker #picker></ngx-mat-color-picker>
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output, ViewChild } from '@angular/core';
import { GuiComponentInterface } from '../../gui-component.interface';
import { NgxMatColorPickerComponent } from '@angular-material-components/color-picker/lib/components/color-picker/color-picker.component';
import { Color } from '@angular-material-components/color-picker';
import { GlobalPositionStrategy } from '@angular/cdk/overlay';

@Component({
selector: 'app-palette-block-ui',
Expand All @@ -17,10 +20,43 @@ export class PaletteBlockUiComponent implements GuiComponentInterface {

@Output('changed') changed: EventEmitter<void> = new EventEmitter<void>();

@ViewChild('picker') picker!: NgxMatColorPickerComponent;

constructor() {}

lpad(str: string, padString: string, length: number) {
while (str.length < length) str = padString + str;
return str;
}

private selectedIndex: number | null = null;

onColorClicked(em: HTMLDivElement, index: number) {
if (!this.resourceData) {
this.selectedIndex = null;
return;
}
this.selectedIndex = index;
const color = this.resourceData.colors[index] || 0;
this.picker.select(
new Color((color & 0xff000000) >>> 24, (color & 0xff0000) >>> 16, (color & 0xff00) >>> 8, color & 0xff),
);
this.picker.open();
const ps = new GlobalPositionStrategy();
ps.top(Math.min(em.offsetTop, window.innerHeight - 450) + 'px');
ps.left(Math.min(em.offsetLeft, window.innerWidth - 380) + 'px');
this.picker._popupRef.updatePositionStrategy(ps);
ps.apply();
}

onColorChange(color: Color | null) {
if (!this.resourceData) {
this.selectedIndex = null;
return;
}
if (this.selectedIndex !== null) {
this.resourceData.colors[this.selectedIndex] = color ? parseInt(color.toHex8String().substring(1), 16) : 0;
this.changed.emit();
}
}
}
17 changes: 14 additions & 3 deletions library/loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,9 +131,17 @@ def probe_block_class(binary_file: [BufferedReader, BytesIO], file_path: str = N
raise NotImplementedError('Don`t have parser for such resource')


def path_to_name(path: str) -> str:
return path.replace('\\', '/').replace(':', '---DRIVE')


def id_to_path(id: str) -> str:
return id.split('__')[0].replace('---DRIVE', ':')


# id example: /media/data/nfs/SIMDATA/CARFAMS/LDIABL.CFM__1/frnt
def require_resource(id: str) -> Tuple[Tuple[str, "DataBlock", dict], Tuple[str, "DataBlock", dict]]:
file_path = id.split('__')[0].replace('---DRIVE', ':')
file_path = id_to_path(id)
(file_id, block, data) = require_file(file_path)
if not data:
return (id, None, None), (file_id, None, None)
Expand All @@ -154,13 +162,16 @@ def require_resource(id: str) -> Tuple[Tuple[str, "DataBlock", dict], Tuple[str,

def clear_file_cache(path: str):
try:
del files_cache[path.replace('\\', '/')]
name = path_to_name(path)
del files_cache[name]
from library.read_blocks import DataBlock
DataBlock.root_read_ctx.children = [c for c in DataBlock.root_read_ctx.children if c.name != name]
except KeyError:
pass


def require_file(path: str) -> Tuple[str, "DataBlock", dict]:
name = path.replace('\\', '/').replace(':', '---DRIVE')
name = path_to_name(path)
(block, data) = files_cache.get(name, (None, None))
if block is None or data is None:
with open(path, 'rb', buffering=100 * 1024 * 1024) as bdata:
Expand Down
14 changes: 4 additions & 10 deletions resources/eac/fields/colors.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

from library.context import ReadContext, WriteContext
from library.read_blocks import IntegerBlock, DataBlock
from library.utils import transform_bitness, transform_color_bitness
from library.utils import transform_color_bitness


class Color24BitDosBlock(IntegerBlock):
Expand All @@ -20,17 +20,11 @@ def __init__(self, **kwargs):
def read(self, buffer: [BufferedReader, BytesIO], ctx: ReadContext = DataBlock.root_read_ctx, name: str = '',
read_bytes_amount=None):
number = super().read(buffer, ctx, name)
red = transform_bitness((number & 0xFF0000) >> 16, 6)
green = transform_bitness((number & 0xFF00) >> 8, 6)
blue = transform_bitness(number & 0xFF, 6)
return red << 24 | green << 16 | blue << 8 | 255
return (number & 0x3F3F3F) << 10 | 255

def write(self, data, ctx: WriteContext = None, name: str = '') -> bytes:
red = (data & 0xff000000) >> 26
green = (data & 0xff0000) >> 18
blue = (data & 0xff00) >> 10
value = red << 16 | green << 8 | blue
return super().write(value, ctx, name)
number = (data & 0xFCFCFC00) >> 10
return super().write(number, ctx, name)


class Color24BitBlock(IntegerBlock):
Expand Down
43 changes: 9 additions & 34 deletions resources/eac/palettes.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from abc import ABC
from io import BufferedReader, BytesIO
from typing import Dict
from typing import Dict, Tuple, Any

from library.context import ReadContext
from library.read_blocks import DeclarativeCompoundBlock, BytesBlock, ArrayBlock, IntegerBlock, DataBlock
Expand All @@ -11,38 +11,6 @@
Color16Bit0565Block, Color16BitDosBlock,
)

transparency_colors = [
# default
0xFF_00_FF_FF,
0x00_FF_00_FF,
0x00_00_FF_FF,
# green-ish
0x00_EA_1C_FF, # TNFS lost vegas map props
0x00_EB_1C_FF, # TNFS lost vegas map props
# 0x00_FB_00_FF, # NFS2 GAMEDATA/TRACKS/SE/TR050M, not working, there is Bitmap 0565 without alpha
0x04_FF_00_FF,
0x0C_FF_00_FF,
0x24_ff_10_FF, # TNFS TRAFFC.CFM
0x28_FF_28_FF,
0x28_FF_2C_FF,
# blue
0x00_00_FC_FF, # TNFS Porsche 911 CFM
# light blue
0x00_FF_FF_FF,
0x1a_ff_ff_ff, # NFS2SE TRACKS/PC/TR000M.QFS
0x48_ff_ff_FF, # NFS2SE TRACKS/PC/TR020M.QFS
# purple
0xCE_1C_C6_FF, # some TNFS map props
0xF2_00_FF_FF,
0xFF_00_F7_FF, # TNFS AL2 map props
0xFF_00_F6_FF, # TNFS NTRACKFM/AL3_T01.FAM map props
0xFF_31_59_FF, # TNFS ETRACKFM/CL3_001.FAM road sign
# gray
0x28_28_28_FF, # car wheels
0xFF_FF_FF_FF, # map props
0x00_00_00_FF, # some menu items: SHOW/DIABLO.QFS
]


class BasePalette(DeclarativeCompoundBlock, ABC):
can_use_last_color_as_transparent = True
Expand All @@ -61,14 +29,21 @@ def new_data(self):
return {**super().new_data(),
'last_color_transparent': False}

def get_child_block_with_data(self, unpacked_data: dict, name: str) -> Tuple['DataBlock', Any]:
if name == 'last_color_transparent':
return None, unpacked_data['last_color_transparent']
return super().get_child_block_with_data(unpacked_data, name)

def read(self, buffer: [BufferedReader, BytesIO], ctx: ReadContext = DataBlock.root_read_ctx, name: str = '',
read_bytes_amount=None):
res = super().read(buffer, ctx, name)
if res.get('num_colors') is not None:
assert res['num_colors'] == res['num_colors1']
res['last_color_transparent'] = False
try:
if self.can_use_last_color_as_transparent and res['colors'][255] in transparency_colors:
# I'm not sure how game decides whether it should draw 255th color transparent or not.
# It appears that only qfs files in SLIDES/GSLIDES get broken if apply transparency to all bitmaps
if self.can_use_last_color_as_transparent and len(res['colors']) >= 256 and 'SLIDES/' not in ctx.ctx_path:
res['last_color_transparent'] = True
except IndexError:
pass
Expand Down
12 changes: 12 additions & 0 deletions test/resources/eac/test_fields.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import unittest
from io import BytesIO

from resources.eac.fields.colors import Color24BitDosBlock
from resources.eac.fields.numbers import Nfs1Angle14, Nfs1Angle8


Expand All @@ -17,3 +18,14 @@ def test_angle_8_should_have_correct_rounding(self):
raw = field.unpack(BytesIO(bytes([11])))
serialized = field.pack(raw)
self.assertListEqual(list(serialized), [11])

def test_color_24bit_dos_should_be_translated_correctly(self):
field = Color24BitDosBlock()
color = field.unpack(BytesIO(bytes([0b0010_1010, 0b0001_0100, 0b0011_1011])))
self.assertEqual(color, 0b10101000_01010000_11101100_11111111)

def test_color_24bit_dos_should_be_saved_correctly(self):
field = Color24BitDosBlock()
color = 0b11101010_01010000_10101101_11111111
serialized = field.pack(color)
self.assertListEqual(list(serialized), [0b0011_1010, 0b0001_0100, 0b0010_1011])

0 comments on commit 41ae979

Please sign in to comment.