mirror of
https://github.com/sehugg/8bitworkshop.git
synced 2024-06-08 08:33:32 +00:00
working on pixel, palette editors
This commit is contained in:
parent
21bc4fd1e5
commit
318fa399a7
23
css/ui.css
23
css/ui.css
|
@ -460,6 +460,9 @@ div.asset_file {
|
|||
div.asset_file_header {
|
||||
font-weight: bold;
|
||||
color: white;
|
||||
background-color: #333;
|
||||
border-radius: 8px;
|
||||
padding-left: 1em;
|
||||
}
|
||||
div.asset_grid {
|
||||
line-height:0;
|
||||
|
@ -468,16 +471,28 @@ div.asset_grid {
|
|||
div.asset_grid span {
|
||||
white-space: nowrap;
|
||||
}
|
||||
div.asset_grid canvas {
|
||||
.asset_cell {
|
||||
padding: 0px;
|
||||
border: 1px solid black;
|
||||
}
|
||||
div.asset_grid canvas:hover {
|
||||
.asset_cell:hover {
|
||||
border: 1px solid white;
|
||||
}
|
||||
.asset_editing {
|
||||
border: 1px dotted white !important;
|
||||
box-shadow: 0px 0px 1em rgba(255,255,255,1);
|
||||
}
|
||||
td.asset_editable {
|
||||
padding:0.3em;
|
||||
font-family: "Andale Mono", "Menlo", "Lucida Console", monospace;
|
||||
}
|
||||
div.asset_dual {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
div.asset_dual table {
|
||||
border-spacing: 10px;
|
||||
border-collapse: separate;
|
||||
}
|
||||
div.asset_editor {
|
||||
border-radius:16px;
|
||||
|
|
|
@ -97,6 +97,8 @@ TODO:
|
|||
- better undo/diff for mistakes?
|
||||
- ide bug/feature visualizer for sponsors
|
||||
- optimization flags for sdcc (oldralloc)
|
||||
- 'src is undefined' when committing old image editor
|
||||
- editor: select palette for chr, select charmap for map (dependencies?)
|
||||
|
||||
|
||||
WEB WORKER FORMAT
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
// link the pattern table into CHR ROM
|
||||
//#link "chr_generic.s"
|
||||
|
||||
const char PALETTE[16] = {
|
||||
const char PALETTE[16] = { /*{pal:"nes",layout:"nes"}*/
|
||||
0x03,
|
||||
0x11,0x30,0x27, 0,
|
||||
0x1c,0x20,0x2c, 0,
|
||||
|
|
|
@ -726,7 +726,7 @@ void play_scene() {
|
|||
rescue_scene();
|
||||
}
|
||||
|
||||
const char PALETTE[32] = {
|
||||
const char PALETTE[32] = { /*{pal:"nes",layout:"nes"}*/
|
||||
0x03, // background color
|
||||
|
||||
0x11,0x30,0x27, 0, // ladders and pickups
|
||||
|
|
|
@ -2,9 +2,12 @@
|
|||
.export _climbr_title_rle
|
||||
|
||||
_climbr_title_pal:
|
||||
.byte $0f,$11,$25,$35,$0f,$01,$21,$30
|
||||
.byte $0f,$06,$1c,$3c,$0f,$11,$28,$38
|
||||
;;{pal:"nes",layout:"nes"};;
|
||||
.byte $0F,$11,$25,$35,$0F,$01,$21,$30
|
||||
.byte $0F,$06,$1C,$3C,$0F,$11,$28,$38
|
||||
;;
|
||||
_climbr_title_rle:
|
||||
;;{w:32,h:30,bpp:8,comp:"rletag",map:"nesnt"};;
|
||||
.byte $01,$00,$01,$10,$80,$01,$02,$00
|
||||
.byte $80,$00,$80,$00,$01,$1f,$80,$80
|
||||
.byte $00,$01,$07,$41,$4e,$00,$38,$42
|
||||
|
@ -77,3 +80,4 @@ _climbr_title_rle:
|
|||
.byte $02,$75,$00,$01,$02,$55,$01,$04
|
||||
.byte $00,$01,$02,$05,$01,$03,$05,$01
|
||||
.byte $00
|
||||
;;
|
||||
|
|
|
@ -58,7 +58,7 @@ const unsigned char* const playerRunSeq[16] = {
|
|||
playerRRun1, playerRRun2,
|
||||
};
|
||||
|
||||
const char PALETTE[32] = {
|
||||
const char PALETTE[32] = { /*{pal:"nes",layout:"nes"}*/
|
||||
0x03, // background color
|
||||
|
||||
0x11,0x30,0x27, 0, // ladders and pickups
|
||||
|
|
|
@ -102,7 +102,7 @@ void scroll_demo() {
|
|||
}
|
||||
}
|
||||
|
||||
const char PALETTE[32] = {
|
||||
const char PALETTE[32] = { /*{pal:"nes",layout:"nes"}*/
|
||||
0x03, // background color
|
||||
|
||||
0x11,0x30,0x27, 0, // background 0
|
||||
|
|
|
@ -58,7 +58,7 @@ const unsigned char* const playerRunSeq[16] = {
|
|||
playerRRun1, playerRRun2,
|
||||
};
|
||||
|
||||
const char PALETTE[32] = {
|
||||
const char PALETTE[32] = { /*{pal:"nes",layout:"nes"}*/
|
||||
0x03, // background color
|
||||
|
||||
0x11,0x30,0x27, 0, // ladders and pickups
|
||||
|
|
|
@ -58,18 +58,18 @@ const unsigned char* const playerRunSeq[16] = {
|
|||
playerRRun1, playerRRun2,
|
||||
};
|
||||
|
||||
const char PALETTE[32] = {
|
||||
const char PALETTE[32] = { /*{pal:"nes",layout:"nes"}*/
|
||||
0x03, // background color
|
||||
|
||||
0x11,0x30,0x27, 0, // ladders and pickups
|
||||
0x1c,0x20,0x2c, 0, // floor blocks
|
||||
0x00,0x10,0x20, 0,
|
||||
0x06,0x16,0x26, 0,
|
||||
0x25,0x30,0x27,0x00, // ladders and pickups
|
||||
0x1C,0x20,0x2C,0x00, // floor blocks
|
||||
0x00,0x10,0x20,0x00,
|
||||
0x06,0x16,0x26,0x00,
|
||||
|
||||
0x16,0x35,0x24, 0, // enemy sprites
|
||||
0x00,0x37,0x25, 0, // rescue person
|
||||
0x0d,0x2d,0x3a, 0,
|
||||
0x0d,0x27,0x2a // player sprites
|
||||
0x16,0x35,0x24,0x00, // enemy sprites
|
||||
0x00,0x37,0x25,0x00, // rescue person
|
||||
0x0D,0x2D,0x1A,0x00,
|
||||
0x0D,0x27,0x2A // player sprites
|
||||
};
|
||||
|
||||
// setup PPU and tables
|
||||
|
|
|
@ -58,7 +58,7 @@ const unsigned char* const playerRunSeq[16] = {
|
|||
playerRRun1, playerRRun2,
|
||||
};
|
||||
|
||||
const char PALETTE[32] = {
|
||||
const char PALETTE[32] = { /*{pal:"nes",layout:"nes"}*/
|
||||
0x03, // background color
|
||||
|
||||
0x11,0x30,0x27, 0, // ladders and pickups
|
||||
|
|
|
@ -25,7 +25,7 @@
|
|||
|
||||
//#define DEBUG_FRAMERATE
|
||||
|
||||
const char PALETTE[32] = {
|
||||
const char PALETTE[32] = { /*{pal:"nes",layout:"nes"}*/
|
||||
0x0f,
|
||||
|
||||
0x11,0x24,0x3c, 0,
|
||||
|
|
|
@ -251,7 +251,7 @@ AE(1,1,1,1),AE(1,1,1,1),AE(1,1,1,1),AE(1,1,1,1), AE(1,1,1,1),AE(1,1,1,1),AE(1,1,
|
|||
};
|
||||
|
||||
// this is palette data
|
||||
const unsigned char Palette_Table[16]={
|
||||
const unsigned char Palette_Table[16]={ /*{pal:"nes",layout:"nes"}*/
|
||||
0x02,
|
||||
0x31,0x31,0x31,0x00,
|
||||
0x34,0x34,0x34,0x00,
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
//#link "chr_generic.s"
|
||||
|
||||
|
||||
const char PALETTE[32] = {
|
||||
const char PALETTE[32] = { /*{pal:"nes",layout:"nes"}*/
|
||||
0x03, // background color
|
||||
|
||||
0x11,0x30,0x27, 0, // ladders and pickups
|
||||
|
|
|
@ -664,6 +664,7 @@ DigitsBitmap ;;{w:8,h:5,count:10,brev:1};;
|
|||
.byte $EE,$22,$22,$22,$22
|
||||
.byte $EE,$AA,$EE,$AA,$EE
|
||||
.byte $EE,$AA,$EE,$22,$EE
|
||||
;;end
|
||||
|
||||
; Playfield bitmasks for all 40 brick columns
|
||||
PFMaskTable
|
||||
|
|
|
@ -167,6 +167,7 @@ NUMBERS ;;{w:8,h:6,count:10,brev:1};;
|
|||
.byte $EE,$22,$22,$22,$22,$00
|
||||
.byte $EE,$AA,$EE,$AA,$EE,$00
|
||||
.byte $EE,$AA,$EE,$22,$EE,$00
|
||||
;;end
|
||||
|
||||
; Epilogue
|
||||
org $fffc
|
||||
|
|
|
@ -151,6 +151,7 @@ DigitsBitmap ;;{w:8,h:5,count:10,brev:1};;
|
|||
.byte $EE,$22,$22,$22,$22
|
||||
.byte $EE,$AA,$EE,$AA,$EE
|
||||
.byte $EE,$AA,$EE,$22,$EE
|
||||
;;end
|
||||
|
||||
; Epilogue
|
||||
org $fffc
|
||||
|
|
|
@ -114,6 +114,7 @@ NUMBERS ;;{w:8,h:6,count:10,brev:1};;
|
|||
.byte $EE,$22,$22,$22,$22,$00
|
||||
.byte $EE,$AA,$EE,$AA,$EE,$00
|
||||
.byte $EE,$AA,$EE,$22,$EE,$00
|
||||
;; end
|
||||
|
||||
; Epilogue
|
||||
org $fffc
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
"use strict";
|
||||
|
||||
import { hex } from "../util";
|
||||
import { CodeProject } from "../project";
|
||||
import { hex, rgb2bgr, rle_unpack } from "../util";
|
||||
import { ProjectWindows } from "../windows";
|
||||
|
||||
export type UintArray = number[] | Uint8Array | Uint16Array | Uint32Array; //{[i:number]:number};
|
||||
|
||||
export type PixelEditorImageFormat = {
|
||||
w:number
|
||||
|
@ -24,6 +26,8 @@ export type PixelEditorPaletteFormat = {
|
|||
layout?:string
|
||||
};
|
||||
|
||||
export type PixelEditorPaletteLayout = [string, number, number][];
|
||||
|
||||
type PixelEditorMessage = {
|
||||
fmt : PixelEditorImageFormat
|
||||
palfmt : PixelEditorPaletteFormat
|
||||
|
@ -87,20 +91,13 @@ export function PixelEditor(parentDiv:HTMLElement,
|
|||
|
||||
updateImage();
|
||||
|
||||
function revrgb(x) {
|
||||
var y = 0;
|
||||
y |= ((x >> 0) & 0xff) << 16;
|
||||
y |= ((x >> 8) & 0xff) << 8;
|
||||
y |= ((x >> 16) & 0xff) << 0;
|
||||
return y;
|
||||
}
|
||||
|
||||
this.createPaletteButtons = function() {
|
||||
var span = $("#palette_group").empty();
|
||||
for (var i=0; i<palette.length; i++) {
|
||||
var btn = $('<button class="palbtn">');
|
||||
var rgb = palette[i] & 0xffffff;
|
||||
var color = "#" + hex(revrgb(rgb), 6);
|
||||
var color = "#" + hex(rgb2bgr(rgb), 6);
|
||||
btn.click(this.setCurrentColor.bind(this, i));
|
||||
btn.attr('id', 'palcol_' + i);
|
||||
btn.css('backgroundColor', color).text(i.toString(16));
|
||||
|
@ -278,7 +275,7 @@ export function parseHexWords(s:string) : number[] {
|
|||
return arr;
|
||||
}
|
||||
|
||||
export function replaceHexWords(s:string, words:number[]) : string {
|
||||
export function replaceHexWords(s:string, words:UintArray) : string {
|
||||
var result = "";
|
||||
var m;
|
||||
var li = 0;
|
||||
|
@ -325,7 +322,7 @@ function remapBits(x:number, arr:number[]) : number {
|
|||
return y;
|
||||
}
|
||||
|
||||
function convertWordsToImages(words:number[] | Uint8Array, fmt:PixelEditorImageFormat) : Uint8Array[] {
|
||||
function convertWordsToImages(words:UintArray, fmt:PixelEditorImageFormat) : Uint8Array[] {
|
||||
var width = fmt.w;
|
||||
var height = fmt.h;
|
||||
var count = fmt.count || 1;
|
||||
|
@ -402,7 +399,7 @@ function convertImagesToWords(images:Uint8Array[], fmt:PixelEditorImageFormat) :
|
|||
}
|
||||
|
||||
// TODO
|
||||
function convertPaletteBytes(arr:number[]|Uint8Array,r0,r1,g0,g1,b0,b1) : number[] {
|
||||
function convertPaletteBytes(arr:UintArray,r0,r1,g0,g1,b0,b1) : number[] {
|
||||
var result = [];
|
||||
for (var i=0; i<arr.length; i++) {
|
||||
var d = arr[i];
|
||||
|
@ -428,7 +425,24 @@ export var currentPaletteStr : string;
|
|||
export var currentPaletteFmt : PixelEditorPaletteFormat;
|
||||
export var allthumbs;
|
||||
|
||||
function convertPaletteFormat(palbytes: number[]|Uint8Array, palfmt: PixelEditorPaletteFormat) : number[] {
|
||||
export function getPaletteLength(palfmt: PixelEditorPaletteFormat) : number {
|
||||
var pal = palfmt.pal;
|
||||
if (typeof pal === 'number') {
|
||||
var rr = Math.floor(Math.abs(pal/100) % 10);
|
||||
var gg = Math.floor(Math.abs(pal/10) % 10);
|
||||
var bb = Math.floor(Math.abs(pal) % 10);
|
||||
return 1<<(rr+gg+bb);
|
||||
} else {
|
||||
var paltable = PREDEF_PALETTES[pal];
|
||||
if (paltable) {
|
||||
return paltable.length;
|
||||
} else {
|
||||
throw new Error("No palette named " + pal);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function convertPaletteFormat(palbytes:UintArray, palfmt: PixelEditorPaletteFormat) : number[] {
|
||||
var pal = palfmt.pal;
|
||||
var newpalette;
|
||||
if (typeof pal === 'number') {
|
||||
|
@ -436,14 +450,14 @@ function convertPaletteFormat(palbytes: number[]|Uint8Array, palfmt: PixelEditor
|
|||
var gg = Math.floor(Math.abs(pal/10) % 10);
|
||||
var bb = Math.floor(Math.abs(pal) % 10);
|
||||
// TODO: n
|
||||
if (currentPaletteFmt.pal >= 0)
|
||||
if (pal >= 0)
|
||||
newpalette = convertPaletteBytes(palbytes, 0, rr, rr, gg, rr+gg, bb);
|
||||
else
|
||||
newpalette = convertPaletteBytes(palbytes, rr+gg, bb, rr, gg, 0, rr);
|
||||
} else {
|
||||
var paltable = PREDEF_PALETTES[pal];
|
||||
if (paltable) {
|
||||
newpalette = new Uint32Array(palbytes).map((i) => { return paltable[i & (paltable.length-1)]; });
|
||||
newpalette = new Uint32Array(palbytes).map((i) => { return paltable[i & (paltable.length-1)] | 0xff000000; });
|
||||
} else {
|
||||
throw new Error("No palette named " + pal);
|
||||
}
|
||||
|
@ -584,93 +598,91 @@ function pixelEditorKeypress(e) {
|
|||
// TODO: reversed?
|
||||
var PREDEF_PALETTES = {
|
||||
'nes':[
|
||||
0xFF7C7C7C ,0xFF0000FC ,0xFF0000BC ,0xFF4428BC ,0xFF940084 ,0xFFA80020 ,0xFFA81000 ,0xFF881400
|
||||
,0xFF503000 ,0xFF007800 ,0xFF006800 ,0xFF005800 ,0xFF004058 ,0xFF000000 ,0xFF000000 ,0xFF000000
|
||||
,0xFFBCBCBC ,0xFF0078F8 ,0xFF0058F8 ,0xFF6844FC ,0xFFD800CC ,0xFFE40058 ,0xFFF83800 ,0xFFE45C10
|
||||
,0xFFAC7C00 ,0xFF00B800 ,0xFF00A800 ,0xFF00A844 ,0xFF008888 ,0xFF000000 ,0xFF000000 ,0xFF000000
|
||||
,0xFFF8F8F8 ,0xFF3CBCFC ,0xFF6888FC ,0xFF9878F8 ,0xFFF878F8 ,0xFFF85898 ,0xFFF87858 ,0xFFFCA044
|
||||
,0xFFF8B800 ,0xFFB8F818 ,0xFF58D854 ,0xFF58F898 ,0xFF00E8D8 ,0xFF787878 ,0xFF000000 ,0xFF000000
|
||||
,0xFFFCFCFC ,0xFFA4E4FC ,0xFFB8B8F8 ,0xFFD8B8F8 ,0xFFF8B8F8 ,0xFFF8A4C0 ,0xFFF0D0B0 ,0xFFFCE0A8
|
||||
,0xFFF8D878 ,0xFFD8F878 ,0xFFB8F8B8 ,0xFFB8F8D8 ,0xFF00FCFC ,0xFFF8D8F8 ,0xFF000000 ,0xFF000000
|
||||
0x525252, 0xB40000, 0xA00000, 0xB1003D, 0x740069, 0x00005B, 0x00005F, 0x001840, 0x002F10, 0x084A08, 0x006700, 0x124200, 0x6D2800, 0x000000, 0x000000, 0x000000,
|
||||
0xC4D5E7, 0xFF4000, 0xDC0E22, 0xFF476B, 0xD7009F, 0x680AD7, 0x0019BC, 0x0054B1, 0x006A5B, 0x008C03, 0x00AB00, 0x2C8800, 0xA47200, 0x000000, 0x000000, 0x000000,
|
||||
0xF8F8F8, 0xFFAB3C, 0xFF7981, 0xFF5BC5, 0xFF48F2, 0xDF49FF, 0x476DFF, 0x00B4F7, 0x00E0FF, 0x00E375, 0x03F42B, 0x78B82E, 0xE5E218, 0x787878, 0x000000, 0x000000,
|
||||
0xFFFFFF, 0xFFF2BE, 0xF8B8B8, 0xF8B8D8, 0xFFB6FF, 0xFFC3FF, 0xC7D1FF, 0x9ADAFF, 0x88EDF8, 0x83FFDD, 0xB8F8B8, 0xF5F8AC, 0xFFFFB0, 0xF8D8F8, 0x000000, 0x000000
|
||||
]
|
||||
};
|
||||
|
||||
var PREDEF_LAYOUTS = {
|
||||
'nes_full':[
|
||||
['Screen Color', 1],
|
||||
['Background 1', 3], [null, 1],
|
||||
['Background 2', 3], [null, 1],
|
||||
['Background 3', 3], [null, 1],
|
||||
['Background 4', 3], [null, 1],
|
||||
['Sprite 1', 3], [null, 1],
|
||||
['Sprite 2', 3], [null, 1],
|
||||
['Sprite 3', 3], [null, 1],
|
||||
['Sprite 4', 3]
|
||||
var PREDEF_LAYOUTS : {[id:string]:PixelEditorPaletteLayout} = {
|
||||
'nes':[
|
||||
['Screen Color', 0x00, 1],
|
||||
['Background 1', 0x01, 3],
|
||||
['Background 2', 0x05, 3],
|
||||
['Background 3', 0x09, 3],
|
||||
['Background 4', 0x0d, 3],
|
||||
['Sprite 1', 0x11, 3],
|
||||
['Sprite 2', 0x15, 3],
|
||||
['Sprite 3', 0x19, 3],
|
||||
['Sprite 4', 0x1d, 3]
|
||||
],
|
||||
};
|
||||
|
||||
/////
|
||||
|
||||
export abstract class PixelNode {
|
||||
left : PixelNode; // toward text editor
|
||||
right : PixelNode; // toward pixel editor
|
||||
// TODO: in/out(...) for each type?
|
||||
input?
|
||||
output?
|
||||
export abstract class Node {
|
||||
left : Node; // toward text editor
|
||||
right : Node; // toward pixel editor
|
||||
|
||||
words? : UintArray; // file data
|
||||
images? : Uint8Array[]; // array of indexed image data
|
||||
rgbimgs? : Uint32Array[]; // array of rgba imgages
|
||||
|
||||
abstract updateLeft(); // update coming from right
|
||||
abstract updateRight(); // update coming from left
|
||||
|
||||
refreshLeft() {
|
||||
var p : PixelNode = this;
|
||||
var p : Node = this;
|
||||
while (p) {
|
||||
p.updateLeft();
|
||||
p = p.left;
|
||||
}
|
||||
}
|
||||
refreshRight() {
|
||||
var p : PixelNode = this;
|
||||
var p : Node = this;
|
||||
while (p) {
|
||||
p.updateRight();
|
||||
p = p.right;
|
||||
}
|
||||
}
|
||||
addRight(node : PixelNode) {
|
||||
addRight(node : Node) {
|
||||
this.right = node;
|
||||
node.left = this;
|
||||
}
|
||||
}
|
||||
|
||||
abstract class PixelCodeProjectDataNode extends PixelNode {
|
||||
abstract class CodeProjectDataNode extends Node {
|
||||
project : ProjectWindows;
|
||||
fileid : string;
|
||||
project : CodeProject;
|
||||
words : UintArray;
|
||||
}
|
||||
|
||||
export class PixelFileDataNode extends PixelCodeProjectDataNode {
|
||||
output : Uint8Array;
|
||||
export class FileDataNode extends CodeProjectDataNode {
|
||||
|
||||
constructor(fileid, data) {
|
||||
constructor(project:ProjectWindows, fileid:string, data:Uint8Array) {
|
||||
super();
|
||||
this.project = project;
|
||||
this.fileid = fileid;
|
||||
this.output = data;
|
||||
this.words = data;
|
||||
}
|
||||
updateLeft() {
|
||||
if (this.project) {
|
||||
this.project.updateFile(this.fileid, this.output);
|
||||
this.project.updateFile(this.fileid, this.words as Uint8Array);
|
||||
}
|
||||
}
|
||||
updateRight() {
|
||||
}
|
||||
}
|
||||
|
||||
export class PixelTextDataNode extends PixelCodeProjectDataNode {
|
||||
export class TextDataNode extends CodeProjectDataNode {
|
||||
text : string;
|
||||
start : number;
|
||||
end : number;
|
||||
output : Uint8Array;
|
||||
|
||||
constructor(fileid, text, start, end) {
|
||||
constructor(project:ProjectWindows, fileid:string, text:string, start:number, end:number) {
|
||||
super();
|
||||
this.project = project;
|
||||
this.fileid = fileid;
|
||||
this.text = text;
|
||||
this.start = start;
|
||||
|
@ -678,50 +690,84 @@ export class PixelTextDataNode extends PixelCodeProjectDataNode {
|
|||
}
|
||||
updateLeft() {
|
||||
// TODO: reload editors?
|
||||
var datastr = this.text.substring(this.start, this.end);
|
||||
datastr = replaceHexWords(datastr, this.words);
|
||||
this.text = this.text.substring(0, this.start) + datastr + this.text.substring(this.end);
|
||||
if (this.project) {
|
||||
this.project.updateFile(this.fileid, this.text);
|
||||
//this.project.replaceTextRange(this.fileid, this.start, this.end, datastr);
|
||||
}
|
||||
}
|
||||
updateRight() {
|
||||
var datastr = this.text.substring(this.start, this.end);
|
||||
datastr = convertToHexStatements(datastr); // TODO?
|
||||
var words = parseHexWords(datastr);
|
||||
this.output = new Uint8Array(words); // TODO: 16/32?
|
||||
this.words = words; //new Uint8Array(words); // TODO: 16/32?
|
||||
}
|
||||
}
|
||||
|
||||
export class PixelMapper extends PixelNode {
|
||||
export class Compressor extends Node {
|
||||
|
||||
words : UintArray;
|
||||
|
||||
updateLeft() {
|
||||
// TODO
|
||||
}
|
||||
updateRight() {
|
||||
this.words = rle_unpack(new Uint8Array(this.left.words));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export class Mapper extends Node {
|
||||
|
||||
fmt : PixelEditorImageFormat;
|
||||
input : number[] | Uint8Array;
|
||||
output : Uint8Array[];
|
||||
words : UintArray;
|
||||
images : Uint8Array[];
|
||||
|
||||
updateLeft() {
|
||||
//TODO
|
||||
this.input = convertImagesToWords(this.output, this.fmt);
|
||||
this.images = this.right.images;
|
||||
this.words = convertImagesToWords(this.images, this.fmt);
|
||||
}
|
||||
updateRight() {
|
||||
// convert each word array to images
|
||||
this.input = this.left.output;
|
||||
this.output = convertWordsToImages(this.input, this.fmt);
|
||||
this.words = this.left.words;
|
||||
this.images = convertWordsToImages(this.words, this.fmt);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export class PixelPalettizer extends PixelNode {
|
||||
class RGBAPalette {
|
||||
palcols;
|
||||
constructor(palcols : Uint32Array) {
|
||||
this.palcols = palcols;
|
||||
}
|
||||
indexOf(rgba : number) : number {
|
||||
return this.palcols.find(rgba);
|
||||
}
|
||||
}
|
||||
|
||||
input : Uint8Array[];
|
||||
output : Uint32Array[];
|
||||
export class Palettizer extends Node {
|
||||
|
||||
images : Uint8Array[];
|
||||
rgbimgs : Uint32Array[];
|
||||
palette : Uint32Array;
|
||||
|
||||
updateLeft() {
|
||||
//TODO
|
||||
this.rgbimgs = this.right.rgbimgs;
|
||||
var pal = new RGBAPalette(this.palette);
|
||||
this.images = this.rgbimgs.map( (im:Uint32Array) => {
|
||||
var out = new Uint8Array(im.length);
|
||||
for (var i=0; i<im.length; i++) {
|
||||
out[i] = pal.indexOf(im[i]);
|
||||
}
|
||||
return out;
|
||||
});
|
||||
}
|
||||
updateRight() {
|
||||
this.images = this.left.images;
|
||||
var mask = this.palette.length - 1; // must be power of 2
|
||||
this.input = this.left.output;
|
||||
// for each image, map bytes to RGB colors
|
||||
this.output = this.input.map( (im:Uint8Array) => {
|
||||
this.rgbimgs = this.images.map( (im:Uint8Array) => {
|
||||
var out = new Uint32Array(im.length);
|
||||
for (var i=0; i<im.length; i++) {
|
||||
out[i] = this.palette[im[i] & mask];
|
||||
|
@ -731,7 +777,7 @@ export class PixelPalettizer extends PixelNode {
|
|||
}
|
||||
}
|
||||
|
||||
function dedupPalette(cols : number[]) : Uint32Array {
|
||||
function dedupPalette(cols : UintArray) : Uint32Array {
|
||||
var dup = new Map();
|
||||
var res = new Uint32Array(cols.length);
|
||||
var ndups = 0;
|
||||
|
@ -746,27 +792,35 @@ function dedupPalette(cols : number[]) : Uint32Array {
|
|||
return res;
|
||||
}
|
||||
|
||||
export class PixelPaletteFormatToRGB extends PixelNode {
|
||||
export class PaletteFormatToRGB extends Node {
|
||||
|
||||
input : Uint8Array;
|
||||
output : Uint32Array[];
|
||||
words : UintArray;
|
||||
rgbimgs : Uint32Array[];
|
||||
palette : Uint32Array;
|
||||
palfmt : PixelEditorPaletteFormat;
|
||||
layout : PixelEditorPaletteLayout;
|
||||
|
||||
updateLeft() {
|
||||
//TODO
|
||||
}
|
||||
updateRight() {
|
||||
this.input = this.left.output;
|
||||
this.palette = dedupPalette(convertPaletteFormat(this.input, this.palfmt));
|
||||
this.output = [];
|
||||
this.words = this.left.words;
|
||||
this.palette = dedupPalette(convertPaletteFormat(this.words, this.palfmt));
|
||||
this.layout = PREDEF_LAYOUTS[this.palfmt.layout];
|
||||
this.rgbimgs = [];
|
||||
this.palette.forEach( (rgba:number) => {
|
||||
this.output.push(new Uint32Array([rgba]));
|
||||
this.rgbimgs.push(new Uint32Array([rgba]));
|
||||
});
|
||||
}
|
||||
getAllColors() {
|
||||
var arr = [];
|
||||
for (var i=0; i<getPaletteLength(this.palfmt); i++)
|
||||
arr.push(i);
|
||||
return convertPaletteFormat(arr, this.palfmt);
|
||||
}
|
||||
}
|
||||
|
||||
export class PixelViewer { // TODO: make PixelNode
|
||||
export class Viewer { // TODO: make Node
|
||||
|
||||
width : number;
|
||||
height : number;
|
||||
|
@ -779,7 +833,7 @@ export class PixelViewer { // TODO: make PixelNode
|
|||
this.pixdata = this.ctx.createImageData(this.width, this.height);
|
||||
}
|
||||
|
||||
createWith(pv : PixelViewer) {
|
||||
createWith(pv : Viewer) {
|
||||
this.width = pv.width;
|
||||
this.height = pv.height;
|
||||
this.pixdata = pv.pixdata;
|
||||
|
@ -805,3 +859,42 @@ export class PixelViewer { // TODO: make PixelNode
|
|||
}
|
||||
}
|
||||
|
||||
export class ImageChooser {
|
||||
|
||||
rgbimgs : Uint32Array[];
|
||||
width : number;
|
||||
height : number;
|
||||
|
||||
recreate(parentdiv:JQuery, onclick) {
|
||||
var agrid = $('<div class="asset_grid"/>'); // grid (or 1) of preview images
|
||||
parentdiv.empty().append(agrid);
|
||||
var cscale = Math.max(2, Math.ceil(16/this.width)); // TODO
|
||||
var imgsperline = this.width <= 8 ? 16 : 8; // TODO
|
||||
var span = null;
|
||||
this.rgbimgs.forEach((imdata, i) => {
|
||||
var viewer = new Viewer();
|
||||
viewer.width = this.width;
|
||||
viewer.height = this.height;
|
||||
viewer.recreate();
|
||||
viewer.canvas.style.width = (viewer.width*cscale)+'px'; // TODO
|
||||
viewer.updateImage(imdata);
|
||||
$(viewer.canvas).addClass('asset_cell');
|
||||
$(viewer.canvas).click((e) => {
|
||||
onclick(i, viewer);
|
||||
});
|
||||
if (!span) {
|
||||
span = $('<span/>');
|
||||
agrid.append(span);
|
||||
}
|
||||
span.append(viewer.canvas);
|
||||
var brk = (i % imgsperline) == imgsperline-1;
|
||||
if (brk) {
|
||||
agrid.append($("<br/>"));
|
||||
span = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// TODO: scroll editors into view
|
||||
|
|
|
@ -144,6 +144,7 @@ const _JSNESPlatform = function(mainElement) {
|
|||
else if (flags & KeyFlags.KeyUp)
|
||||
nes.buttonUp(o.index+1, o.mask); // controller, button
|
||||
});
|
||||
//var s = ''; nes.ppu.palTable.curTable.forEach((rgb) => { s += "0x"+hex(rgb,6)+", "; }); console.log(s);
|
||||
}
|
||||
|
||||
advance(novideo : boolean) {
|
||||
|
|
21
src/util.ts
21
src/util.ts
|
@ -423,3 +423,24 @@ export function clamp(minv:number, maxv:number, v:number) {
|
|||
export function safeident(s : string) : string {
|
||||
return s.replace(/\W+/g, "_");
|
||||
}
|
||||
|
||||
export function rle_unpack(src : Uint8Array) : Uint8Array {
|
||||
var i = 0;
|
||||
var tag = src[i++];
|
||||
var dest = [];
|
||||
var data = tag;
|
||||
while (i < src.length) {
|
||||
var ch = src[i++];
|
||||
if (ch == tag) {
|
||||
var count = src[i++];
|
||||
for (var j=0; j<count; j++)
|
||||
dest.push(data);
|
||||
if (count == 0)
|
||||
break;
|
||||
} else {
|
||||
data = ch;
|
||||
dest.push(data);
|
||||
}
|
||||
}
|
||||
return new Uint8Array(dest);
|
||||
}
|
||||
|
|
167
src/views.ts
167
src/views.ts
|
@ -2,13 +2,12 @@
|
|||
|
||||
import $ = require("jquery");
|
||||
//import CodeMirror = require("codemirror");
|
||||
import { CodeProject } from "./project";
|
||||
import { SourceFile, WorkerError, Segment, FileData } from "./workertypes";
|
||||
import { Platform, EmuState, ProfilerOutput, lookupSymbol } from "./baseplatform";
|
||||
import { hex, lpad, rpad, safeident } from "./util";
|
||||
import { hex, lpad, rpad, safeident, rgb2bgr } from "./util";
|
||||
import { CodeAnalyzer } from "./analysis";
|
||||
import { platform, platform_id, compparams, current_project, lastDebugState, projectWindows } from "./ui";
|
||||
import { PixelEditorImageFormat, PixelViewer, PixelMapper, PixelPalettizer, PixelFileDataNode, PixelTextDataNode, PixelPaletteFormatToRGB, parseHexWords } from "./pixed/pixeleditor";
|
||||
import * as pixed from "./pixed/pixeleditor";
|
||||
|
||||
export interface ProjectView {
|
||||
createDiv(parent:HTMLElement, text:string) : HTMLElement;
|
||||
|
@ -343,6 +342,11 @@ export class SourceEditor implements ProjectView {
|
|||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
replaceSelection(start:number, end:number, text:string) {
|
||||
this.editor.setSelection(end, start);
|
||||
this.editor.replaceSelection(text);
|
||||
}
|
||||
|
||||
// bitmap editor (TODO: refactor)
|
||||
|
||||
|
@ -955,6 +959,7 @@ export class ProfileView implements ProjectView {
|
|||
export class AssetEditorView implements ProjectView {
|
||||
maindiv : JQuery;
|
||||
cureditordiv : JQuery;
|
||||
cureditelem : JQuery;
|
||||
|
||||
createDiv(parent : HTMLElement) {
|
||||
this.maindiv = $('<div class="vertical-scroll"/>');
|
||||
|
@ -962,7 +967,7 @@ export class AssetEditorView implements ProjectView {
|
|||
return this.maindiv[0];
|
||||
}
|
||||
|
||||
setCurrentEditor(div : JQuery) {
|
||||
setCurrentEditor(div : JQuery, editing : JQuery) {
|
||||
if (this.cureditordiv != div) {
|
||||
if (this.cureditordiv) {
|
||||
this.cureditordiv.hide(250);
|
||||
|
@ -973,6 +978,14 @@ export class AssetEditorView implements ProjectView {
|
|||
this.cureditordiv.show(250);
|
||||
}
|
||||
}
|
||||
if (this.cureditelem) {
|
||||
this.cureditelem.removeClass('asset_editing');
|
||||
this.cureditelem = null;
|
||||
}
|
||||
if (editing) {
|
||||
this.cureditelem = editing;
|
||||
this.cureditelem.addClass('asset_editing');
|
||||
}
|
||||
}
|
||||
|
||||
scanFileTextForAssets(id : string, data : string) {
|
||||
|
@ -990,7 +1003,7 @@ export class AssetEditorView implements ProjectView {
|
|||
} else {
|
||||
end = data.indexOf(';', start); // C
|
||||
}
|
||||
console.log(id, start, end, m[1]);
|
||||
//console.log(id, start, end, m[1]);
|
||||
if (end > start) {
|
||||
try {
|
||||
var jsontxt = m[1].replace(/([A-Za-z]+):/g, '"$1":'); // fix lenient JSON
|
||||
|
@ -1005,55 +1018,82 @@ export class AssetEditorView implements ProjectView {
|
|||
return result;
|
||||
}
|
||||
|
||||
addPixelEditorViews(filediv : JQuery, images : Uint32Array[], fmt : PixelEditorImageFormat) {
|
||||
addPixelEditorViews(filediv:JQuery, images:Uint32Array[], fmt:pixed.PixelEditorImageFormat) {
|
||||
var adual = $('<div class="asset_dual"/>'); // contains grid and editor
|
||||
var agrid = $('<div class="asset_grid"/>'); // grid (or 1) of preview images
|
||||
var aeditor = $('<div class="asset_editor"/>').hide(); // contains editor, when selected
|
||||
adual.append(agrid, aeditor).appendTo(filediv);
|
||||
|
||||
var chooser = new pixed.ImageChooser();
|
||||
chooser.rgbimgs = images;
|
||||
chooser.width = fmt.w || 1;
|
||||
chooser.height = fmt.h || 1;
|
||||
chooser.recreate(adual, (index, viewer) => {
|
||||
console.log("???",index);
|
||||
var escale = Math.ceil(192/fmt.w);
|
||||
var editview = new pixed.Viewer();
|
||||
editview.createWith(viewer);
|
||||
editview.updateImage(null);
|
||||
editview.canvas.style.width = (viewer.width*escale)+'px'; // TODO
|
||||
aeditor.empty().append(editview.canvas);
|
||||
this.setCurrentEditor(aeditor, $(viewer.canvas));
|
||||
});
|
||||
adual.append(aeditor).appendTo(filediv);
|
||||
}
|
||||
|
||||
addPaletteEditorViews(filediv:JQuery, words, palette, layout, allcolors, callback) {
|
||||
var adual = $('<div class="asset_dual"/>').appendTo(filediv);
|
||||
var aeditor = $('<div class="asset_editor"/>').hide(); // contains editor, when selected
|
||||
// TODO: they need to update when refreshed from right
|
||||
var imgsperline = fmt.w <= 8 ? 16 : 8; // TODO
|
||||
// TODO?
|
||||
var cscale = Math.ceil(16/fmt.w);
|
||||
var escale = Math.ceil(192/fmt.w);
|
||||
var i = 0;
|
||||
var span = null;
|
||||
images.forEach( (imdata) => {
|
||||
var viewer = new PixelViewer();
|
||||
viewer.width = fmt.w | 0;
|
||||
viewer.height = fmt.h | 0;
|
||||
viewer.recreate();
|
||||
viewer.canvas.style.width = (viewer.width*cscale)+'px'; // TODO
|
||||
viewer.updateImage(imdata); // TODO
|
||||
$(viewer.canvas).click((e) => {
|
||||
var editview = new PixelViewer();
|
||||
editview.createWith(viewer);
|
||||
editview.updateImage(null);
|
||||
editview.canvas.style.width = (viewer.width*escale)+'px'; // TODO
|
||||
aeditor.empty().append(editview.canvas);
|
||||
this.setCurrentEditor(aeditor);
|
||||
});
|
||||
if (!span) {
|
||||
span = $('<span/>');
|
||||
agrid.append(span);
|
||||
var allrgbimgs = [];
|
||||
allcolors.forEach((rgba) => { allrgbimgs.push(new Uint32Array([rgba])); }); // array of array of 1 rgb color (for picker)
|
||||
var atable = $('<table/>').appendTo(adual);
|
||||
aeditor.appendTo(adual);
|
||||
// make default layout if not exists
|
||||
if (!layout) {
|
||||
var imgsperline = palette.length > 32 ? 8 : 4;
|
||||
var len = allcolors.length;
|
||||
layout = [];
|
||||
for (var i=0; i<len; i+=imgsperline) {
|
||||
layout.push(["", i, Math.min(len-i,imgsperline)]);
|
||||
}
|
||||
span.append(viewer.canvas);
|
||||
if (++i == imgsperline) {
|
||||
agrid.append($("<br/>"));
|
||||
span = null;
|
||||
i = 0;
|
||||
}
|
||||
// iterate over each row of the layout
|
||||
layout.forEach( ([name, start, len]) => {
|
||||
if (start < palette.length) { // skip row if out of range
|
||||
var arow = $('<tr/>').appendTo(atable);
|
||||
$('<td/>').text(name).appendTo(arow);
|
||||
var inds = [];
|
||||
for (var k=start; k<start+len; k++)
|
||||
inds.push(k);
|
||||
inds.forEach( (i) => {
|
||||
var val = words[i];
|
||||
var rgb = palette[i];
|
||||
var hexcol = '#'+hex(rgb2bgr(rgb),6);
|
||||
var textcol = (rgb & 0x008000) ? 'black' : 'white';
|
||||
var cell = $('<td/>').addClass('asset_cell asset_editable').text(hex(val,2)).css('background-color',hexcol).css('color',textcol).appendTo(arow);
|
||||
cell.click((e) => {
|
||||
var chooser = new pixed.ImageChooser();
|
||||
chooser.rgbimgs = allrgbimgs;
|
||||
chooser.width = 1;
|
||||
chooser.height = 1;
|
||||
chooser.recreate(aeditor, (index, newvalue) => {
|
||||
callback(i, index);
|
||||
});
|
||||
this.setCurrentEditor(aeditor, cell);
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
addPixelEditor(filediv:JQuery, firstnode:PixelFileDataNode|PixelTextDataNode, fmt:PixelEditorImageFormat) {
|
||||
addPixelEditor(filediv:JQuery, firstnode:pixed.Node, fmt:pixed.PixelEditorImageFormat) {
|
||||
// data -> pixels
|
||||
var mapper = new PixelMapper();
|
||||
var mapper = new pixed.Mapper();
|
||||
fmt.xform = 'scale(2)';
|
||||
mapper.fmt = fmt;
|
||||
// TODO: rotate node?
|
||||
firstnode.addRight(mapper);
|
||||
// pixels -> RGBA
|
||||
var palizer = new PixelPalettizer();
|
||||
var palizer = new pixed.Palettizer();
|
||||
if (fmt.bpp*(fmt.np|1) == 1)
|
||||
palizer.palette = new Uint32Array([0xff000000, 0xffffffff]);
|
||||
else
|
||||
|
@ -1061,48 +1101,64 @@ export class AssetEditorView implements ProjectView {
|
|||
mapper.addRight(palizer);
|
||||
// refresh
|
||||
firstnode.refreshRight();
|
||||
// add view objects (TODO)
|
||||
this.addPixelEditorViews(filediv, palizer.output, fmt);
|
||||
// add view objects
|
||||
this.addPixelEditorViews(filediv, palizer.rgbimgs, fmt);
|
||||
}
|
||||
|
||||
addPaletteEditor(filediv:JQuery, firstnode:PixelFileDataNode|PixelTextDataNode, palfmt?) {
|
||||
addPaletteEditor(filediv:JQuery, firstnode:pixed.Node, palfmt?) {
|
||||
// palette -> RGBA
|
||||
var pal2rgb = new PixelPaletteFormatToRGB();
|
||||
var pal2rgb = new pixed.PaletteFormatToRGB();
|
||||
pal2rgb.palfmt = palfmt;
|
||||
firstnode.addRight(pal2rgb);
|
||||
firstnode.refreshRight();
|
||||
// add view objects (TODO)
|
||||
var imgfmt = {w:1,h:1};
|
||||
this.addPixelEditorViews(filediv, pal2rgb.output, imgfmt);
|
||||
// add view objects
|
||||
// TODO: show which one is selected?
|
||||
this.addPaletteEditorViews(filediv, firstnode.words,
|
||||
pal2rgb.palette, pal2rgb.layout, pal2rgb.getAllColors(),
|
||||
(index, newvalue) => {
|
||||
console.log('set entry', index, '=', newvalue);
|
||||
firstnode.words[index] = newvalue;
|
||||
//firstnode.refreshRight();
|
||||
firstnode.refreshLeft();
|
||||
});
|
||||
}
|
||||
|
||||
refreshAssetsInFile(fileid : string, data : FileData) {
|
||||
refreshAssetsInFile(fileid : string, data : FileData) : number {
|
||||
let nassets = 0;
|
||||
let filediv = $('#'+this.getFileDivId(fileid)).empty();
|
||||
// TODO
|
||||
// TODO: check if open
|
||||
if (fileid.endsWith('.chr') && data instanceof Uint8Array) {
|
||||
// is this a NES CHR?
|
||||
let node = new PixelFileDataNode(fileid, data);
|
||||
let node = new pixed.FileDataNode(projectWindows, fileid, data);
|
||||
const neschrfmt = {w:8,h:8,bpp:1,count:(data.length>>4),brev:true,np:2,pofs:8,remap:[0,1,2,4,5,6,7,8,9,10,11,12]}; // TODO
|
||||
this.addPixelEditor(filediv, node, neschrfmt);
|
||||
} else if (typeof data === 'string') {
|
||||
let textfrags = this.scanFileTextForAssets(fileid, data);
|
||||
for (let frag of textfrags) {
|
||||
let node : pixed.Node = new pixed.TextDataNode(projectWindows, fileid, data, frag.start, frag.end);
|
||||
if (frag.fmt.comp == 'rletag') {
|
||||
node.addRight(new pixed.Compressor());
|
||||
node.refreshRight(); // TODO
|
||||
node = node.right;
|
||||
console.log(node);
|
||||
}
|
||||
// is this a bitmap?
|
||||
if (frag.fmt && frag.fmt.w > 0 && frag.fmt.h > 0) {
|
||||
let node = new PixelTextDataNode(fileid, data, frag.start, frag.end);
|
||||
this.addPixelEditor(filediv, node, frag.fmt);
|
||||
nassets++;
|
||||
}
|
||||
// is this a palette?
|
||||
else if (frag.fmt && frag.fmt.pal) {
|
||||
let node = new PixelTextDataNode(fileid, data, frag.start, frag.end);
|
||||
this.addPaletteEditor(filediv, node, frag.fmt);
|
||||
nassets++;
|
||||
}
|
||||
else {
|
||||
// TODO: other kinds of resources?
|
||||
}
|
||||
}
|
||||
}
|
||||
return nassets;
|
||||
}
|
||||
|
||||
getFileDivId(id : string) {
|
||||
|
@ -1117,8 +1173,13 @@ export class AssetEditorView implements ProjectView {
|
|||
var header = $('<div class="asset_file_header"/>').text(id);
|
||||
var body = $('<div/>').attr('id',divid);
|
||||
var filediv = $('<div class="asset_file"/>').append(header, body).appendTo(this.maindiv);
|
||||
this.refreshAssetsInFile(id, data);
|
||||
// TODO: what if crash while parsing?
|
||||
try {
|
||||
var nassets = this.refreshAssetsInFile(id, data);
|
||||
if (nassets == 0) filediv.hide();
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
filediv.text(e+""); // TODO: error msg?
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -2,14 +2,14 @@
|
|||
|
||||
import $ = require("jquery");
|
||||
import { CodeProject } from "./project";
|
||||
import { WorkerError } from "./workertypes";
|
||||
import { WorkerError, FileData } from "./workertypes";
|
||||
import { ProjectView } from "./views";
|
||||
|
||||
type WindowCreateFunction = (id:string) => ProjectView;
|
||||
|
||||
export class ProjectWindows {
|
||||
containerdiv:HTMLElement;
|
||||
project:CodeProject;
|
||||
containerdiv : HTMLElement;
|
||||
project : CodeProject;
|
||||
id2window : {[id:string]:ProjectView} = {};
|
||||
id2createfn : {[id:string]:WindowCreateFunction} = {};
|
||||
id2div : {[id:string]:HTMLElement} = {};
|
||||
|
@ -103,4 +103,17 @@ export class ProjectWindows {
|
|||
this.createOrShow(this.activeid);
|
||||
}
|
||||
}
|
||||
|
||||
updateFile(fileid : string, data : FileData) {
|
||||
var wnd = this.id2window[fileid];
|
||||
if (wnd && wnd.setText && typeof data === 'string') {
|
||||
wnd.setText(data);
|
||||
} else {
|
||||
this.project.updateFile(fileid, data);
|
||||
if (wnd) {
|
||||
wnd.refresh(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
|
|
2
tss
2
tss
|
@ -1 +1 @@
|
|||
Subproject commit 5b5ee67fc06956bc7dce51726e98812d2d897eaa
|
||||
Subproject commit 61a1691a1de05dca3b694bf603db49ffbaf572cf
|
Loading…
Reference in New Issue
Block a user