Seren
Dec 4, 2016, 08:34 AM
For JJ2 levels, as long as you have the time it takes, I'm not aware of any better tools than my script (see here (https://www.jazz2online.com/jcf/showpost.php?p=488235&postcount=4146) for usage details, although use the version in this post for more accurate results):
#pragma name "Full Level Screenshot"
jjPALCOLOR getColor(jjPALCOLOR dest, int index, SPRITE::Mode mode, int param) {
jjPALCOLOR source = jjPalette.color[index];
switch (mode) {
case SPRITE::BRIGHTNESS:
return param < 128 ?
jjPALCOLOR(dest.red * param >> 7, dest.green * param >> 7, dest.blue * param >> 7) :
jjPALCOLOR(~(~dest.red * param >> 7), ~(~dest.green * param >> 7), ~(~dest.blue * param >> 7));
case SPRITE::FROZEN:
{
jjPALCOLOR color(dest.red + source.red * 3 >> 2, dest.green + source.green * 3 >> 2, dest.blue + source.blue * 3 >> 2);
int light = color.getLight();
return jjPALCOLOR(light >> 3, light * 5 >> 3, light);
}
case SPRITE::GEM:
{
const array<int> hue = {245, 106, 136, 201};
jjPALCOLOR color;
color.setHSL(hue[param & 3], 255, index << 1);
return jjPALCOLOR(dest.red + color.red * 3 >> 2, dest.green + color.green * 3 >> 2, dest.blue + color.blue * 3 >> 2);
}
case SPRITE::INVISIBLE:
return dest;
case SPRITE::PALSHIFT:
return jjPalette.color[(index + param) & 255];
case SPRITE::PLAYER:
{
jjPLAYER@ player = jjPlayers[param];
CHAR::Char chara = player.charCurr;
bool inRange0 = index >= (jjIsTSF && chara == CHAR::LORI ? 24 : 16) && index < 48;
bool inRange1 = index >= 88 && index < 96 && jjIsTSF && chara == CHAR::LORI;
bool inRange2 = index >= 80 && index < 88 && (chara == CHAR::BIRD || chara == CHAR::BIRD2 || chara == CHAR::FROG);
bool inRange3 = index >= 59 && index < 64 && chara != CHAR::BIRD && chara != CHAR::BIRD2;
if (inRange0 || inRange1 || inRange2 || inRange3) {
array<uint> fur(4);
for (int i = 0; i < 4; i++) {
fur[i] = player.fur >> (i << 3);
}
if (chara == CHAR::BIRD) {
for (int i = 0; i < 2; i++) {
uint temp = fur[i];
fur[i] = fur[i | 2];
fur[i | 2] = temp;
}
}
if (inRange0)
index -= 16;
else if (inRange1)
index -= 88;
else if (inRange2)
index -= 80;
else if (fur[3] != 40)
index -= index < 61 ? 34 : 33;
else
return source;
return jjPalette.color[(fur[index >> 3] + (index & 7)) & 255];
}
}
return source;
case SPRITE::SINGLECOLOR:
return jjPalette.color[param];
case SPRITE::TRANSLUCENT:
return jjPALCOLOR(dest.red + source.red >> 1, dest.green + source.green >> 1, dest.blue + source.blue >> 1);
}
return source;
}
void drawLayers(array<array<jjPALCOLOR>>& image, const array<int> &in layers) {
int width = image.isEmpty() ? 0 : image[0].length(), height = image.length();
int layerCount = layers.length();
array<jjPALCOLOR> translucentPixels;
for (int i = 0, j = 0; j < height; (i++) == (j += 32)) {
for (int k = 0, l = 0; l < width; (k++) == (l += 32)) {
array<int> tiles(layerCount, -1);
for (int m = 0; m < 32; m++) {
array<jjPALCOLOR>@ row = image[j | m];
for (int n = 0; n < 32; n++) {
int column = l | n;
for (int o = 0; o < layerCount; o++) {
int tile = tiles[o];
if (tile < 0) {
int layer = layers[o];
int layerWidth = jjLayerWidth[layer], layerHeight = jjLayerHeight[layer];
int x = k, y = i;
if (x >= layerWidth) {
if (jjLayerTileWidth[layer])
x %= layerWidth;
else
x = -1;
}
if (y >= layerHeight) {
if (jjLayerTileHeight[layer])
y %= layerHeight;
else
y = -1;
}
tile = tiles[o] = x >= 0 && y >= 0 ? jjTileGet(layer, x, y) : 0;
}
int index = jjPIXELMAP(jjGetStaticTile(tile))[n, m];
if (index != 0) {
jjPALCOLOR color = jjPalette.color[index];
if (tile & TILE::ANIMATED == 0 && jjTileType[tile & TILE::RAWRANGE] == 1) {
translucentPixels.insertLast(color);
} else {
row[column] = color;
break;
}
}
}
while (!translucentPixels.isEmpty()) {
jjPALCOLOR one = translucentPixels[translucentPixels.length() - 1], two = row[column];
row[column] = jjPALCOLOR(one.red * 3 + two.red >> 2, one.green * 3 + two.green >> 2, one.blue * 3 + two.blue >> 2);
translucentPixels.removeLast();
}
}
}
}
}
}
void drawFrame(array<array<jjPALCOLOR>>& image, jjANIMFRAME@ frame, int x, int y, int d, SPRITE::Mode mode, int param) {
const jjPIXELMAP source(frame);
int width = frame.width, height = frame.height;
int xMax = image.isEmpty() ? 0 : image[0].length(), yMax = image.length();
bool hFlip = d < 0, vFlip = (d & 0xC0 != 0) && (~d & 0xC0 != 0);
x += hFlip ? 1 - (width + frame.hotSpotX) : frame.hotSpotX;
y += vFlip ? 1 - (height + frame.hotSpotY) : frame.hotSpotY;
int xDir = hFlip ? -1 : 1;
int yDir = vFlip ? -1 : 1;
int xStart = hFlip ? width - 1 : 0;
int yStart = vFlip ? height - 1 : 0;
for (int i = 0, j = y, k = yStart; i < height; (i++) ^ (j++) ^ (k += yDir)) {
if (j >= 0 && j < yMax) {
array<jjPALCOLOR>@ row = image[j];
for (int l = 0, m = x, n = xStart; l < width; (l++) ^ (m++) ^ (n += xDir)) {
if (m >= 0 && m < xMax) {
int index = source[n, k];
if (index != 0)
row[m] = getColor(row[m], index, mode, param);
}
}
}
}
}
void takeScreenshot(const string &in filename) {
array<int> fgLayers, bgLayers;
for (int i = 1; i < 8; i++) {
if (
jjLayerHasTiles[i] &&
jjLayerXSpeed[i] == 1.f &&
jjLayerYSpeed[i] == 1.f &&
jjLayerXAutoSpeed[i] == 0.f &&
jjLayerYAutoSpeed[i] == 0.f &&
jjLayerXOffset[i] == 0.f &&
jjLayerYOffset[i] == 0.f
)
(i < 4 ? fgLayers : bgLayers).insertLast(i);
}
bgLayers.insertLast(8);
int width = jjLayerWidth[4] << 5, height = jjLayerHeight[4] << 5;
array<array<jjPALCOLOR>> image(height, array<jjPALCOLOR>(width));
drawLayers(image, bgLayers);
for (int i = 1; i < jjObjectCount; i++) {
const jjOBJ@ obj = jjObjects[i];
if (obj.isActive && obj.curFrame != 0) {
int pickupAnim = obj.curAnim - jjAnimSets[ANIM::PICKUPS];
jjANIMFRAME@ frame = jjAnimFrames[obj.curFrame];
SPRITE::Mode mode;
int param = 0;
if (obj.freeze != 0) {
mode = SPRITE::FROZEN;
} else if (obj.justHit != 0) {
mode = SPRITE::SINGLECOLOR;
param = 15;
} else if (pickupAnim == 22 || pickupAnim == 34 || pickupAnim == 35) {
mode = SPRITE::GEM;
param = obj.var[0] - 1;
} else if (frame.transparent) {
mode = SPRITE::TRANSLUCENT;
} else {
mode = SPRITE::NORMAL;
}
switch (obj.eventID) {
case OBJECT::STOMPSCENERY:
case OBJECT::COLLAPSESCENERY:
case OBJECT::DESTRUCTSCENERY:
case OBJECT::DESTRUCTSCENERYBOMB:
case OBJECT::BUBBLER:
case OBJECT::GEMSTOMP:
break;
case OBJECT::FLOATLIZARD:
{
int x = int(obj.xPos), y = int(obj.yPos);
const jjANIMATION@ anim = jjAnimations[jjAnimSets[ANIM::LIZARD] + 3];
drawFrame(image, jjAnimFrames[anim + jjGameTicks / 3 % anim.frameCount], x, y + 4, obj.direction, mode, param);
drawFrame(image, frame, x, y, obj.direction, mode, param);
}
break;
case OBJECT::WARP:
{
const array<int> palShift = {248, 48, 0, 24, 8, 32, 40};
int x = int(obj.xPos), y = int(obj.yPos);
const jjANIMATION@ anim;
@anim = jjAnimations[obj.special];
drawFrame(image, jjAnimFrames[anim + (obj.counter >> 3) % anim.frameCount], x - 1, y + 3, obj.direction, mode, param);
@anim = jjAnimations[obj.curAnim];
drawFrame(image, jjAnimFrames[obj.curFrame + anim.frameCount], x, y, obj.direction, mode, param);
drawFrame(image, frame, x, y, obj.direction, SPRITE::PALSHIFT, palShift[obj.var[0] % 7]);
}
break;
case OBJECT::BRIDGE:
{
int length = 0, x = int(obj.xPos), y = int(obj.yPos);
uint frameID = 0;
const jjANIMATION@ anim = jjAnimations[obj.curAnim];
do {
@frame = jjAnimFrames[anim + frameID];
drawFrame(image, frame, x + length - frame.hotSpotX, y, obj.direction, mode, param);
length += frame.width;
if (++frameID >= anim.frameCount)
frameID = 0;
} while (length < obj.var[0]);
}
break;
case OBJECT::BOLLPLATFORM:
case OBJECT::FRUITPLATFORM:
case OBJECT::GRASSPLATFORM:
case OBJECT::PINKPLATFORM:
case OBJECT::SONICPLATFORM:
case OBJECT::SPIKEPLATFORM:
case OBJECT::SPIKEBOLL:
{
jjANIMFRAME@ subFrame = jjAnimFrames[jjAnimations[obj.killAnim]];
int distPlus = subFrame.width - 1, dist = 0, anglePlus = -obj.var[5], angle = obj.var[4] - anglePlus * obj.var[2];
for (int j = obj.var[2]; j > 0; j--) {
drawFrame(image, subFrame, int(obj.xOrg + dist * jjSin(angle)), int(obj.yOrg + dist * jjCos(angle)), 0, mode, param);
angle += anglePlus;
dist += distPlus;
}
}
drawFrame(image, frame, int(obj.xPos), int(obj.yPos), obj.direction, mode, param);
break;
case OBJECT::CTFBASE:
{
int animSet = jjAnimSets[ANIM::FLAG], x = int(obj.xOrg), y = int(obj.yOrg), d = obj.var[2] != 0 ? 1 : -1, t = jjGameTicks / 6;
const jjANIMATION@ anim;
@anim = jjAnimations[animSet + 1];
drawFrame(image, jjAnimFrames[anim + obj.var[1]], x + 5 * d, y + 17, d, mode, param);
@anim = jjAnimations[animSet + 2];
drawFrame(image, jjAnimFrames[anim + t % anim.frameCount], x + 23 * d, y - 36, d, mode, param);
if (obj.var[4] == 0) {
@anim = jjAnimations[animSet + 5];
drawFrame(image, jjAnimFrames[anim + t % anim.frameCount], x - 64 * d, y - 4, -d, mode, param);
} else {
@anim = jjAnimations[animSet + 6];
drawFrame(image, jjAnimFrames[anim + obj.counter / 4 % anim.frameCount], x - 64 * d, y - 4, -d, mode, param);
}
}
default:
drawFrame(image, frame, int(obj.xPos), int(obj.yPos), obj.direction, mode, param);
}
}
}
for (int i = 0; i < 32; i++) {
const jjPLAYER@ player = jjPlayers[i];
if (player.isInGame && !player.invisibility)
drawFrame(image, jjAnimFrames[player.curFrame], int(player.xPos), int(player.yPos),
player.direction ^ (player.antiGrav ? SPRITE::FLIPV : SPRITE::FLIPNONE),
player.frozen == 0 ? player.spriteMode : SPRITE::FROZEN, player.spriteParam);
}
drawLayers(image, fgLayers);
jjSTREAM file;
string header = "P6 " + width + ' ' + height + " 255\n";
int headerLength = header.length();
for (int i = 0; i < headerLength; i++) {
file.push(header[i]);
}
for (int i = 0; i < height; i++) {
for (int j = 0; j < width; j++) {
jjPALCOLOR color = image[i][j];
file.push(color.red);
file.push(color.green);
file.push(color.blue);
}
}
file.save(filename);
}
bool onLocalChat(string &in text, CHAT::Type type) {
array<string> results;
if (type == CHAT::NORMAL && jjRegexMatch(text, """!screenshot(?:\s+([^\s"]+|"([^"]+)"))?\s*""", results, true)) {
string filename;
if (results[1].isEmpty())
filename = "SEscreenshot";
else if (results[2].isEmpty())
filename = results[1];
else
filename = results[2];
takeScreenshot(filename + ".ppm.asdat");
return true;
}
return false;
}
Simon
Jan 14, 2017, 08:49 PM
JJ1MOD can generate you full-sized level images: https://www.jazz2online.com/downloads/6040/-/
Thanks! JJ1MOD did the job: Exported Stonar 2 initial section (http://www.lixgame.com/etc/stonar2.png).
JJ1MOD's seems to be halfway command-line, halfway interactive, that made it hard to use in a shell loop. I resorted to running JJ1MOD in DOSBox, trying different levels one after another, giving interactive input (type "1" to select image export) for every level, until I had the desired image.
But it works!
Let me know if JJ1MOD doesn't help, and I could run it on the original levels. It doesn't show events, etc, though.
Happy with the image, but thanks for the offer. :-)
-- Simon
vBulletin® v3.8.2, Copyright ©2000-2025, Jelsoft Enterprises Ltd.