Admops fix
Build and Test j3270 / Build JAR & Run Tests (push) Successful in 1m1s

This commit is contained in:
2026-08-25 22:22:20 +00:00
parent 7eebd2107c
commit 3526e682a1
7 changed files with 445 additions and 38 deletions
@@ -59,6 +59,7 @@ public class DataStreamProcessor {
this.outputBuffer = new byte[32768];
this.outputPos = 0;
this.gocaDecoder.setProgramSymbolManager(programSymbolManager);
this.graphicsPlane.setProgramSymbolManager(programSymbolManager);
}
public QueryReplyBuilder getQueryReplyBuilder() {
@@ -838,6 +839,13 @@ public class DataStreamProcessor {
int orderOffset = (fieldLen >= 7) ? (pos + 7) : (pos + 4);
int orderLen = Math.max(0, fieldLen - (orderOffset - pos));
StringBuilder hexDump = new StringBuilder();
for (int i = 0; i < Math.min(32, fieldLen); i++) {
hexDump.append(String.format("%02x ", data[pos + i] & 0xFF));
}
log.info(String.format("SF 0x0F sub=0x%02x len=%d flags=0x%02x orderOffset=+%d orderLen=%d bytes=[%s]",
sfSubId, fieldLen, flags, (orderOffset - pos), orderLen, hexDump.toString().trim()));
graphicsPlane.setScreenDimensions(screen.getCols(), screen.getRows());
if (flags == 0x80) { // SPAN_FIRST
@@ -856,6 +864,7 @@ public class DataStreamProcessor {
}
byte[] fullStream = gocaAccumulator.toByteArray();
gocaAccumulator.reset();
log.info(String.format("GOCA stream SPAN_LAST assembled: %d bytes", fullStream.length));
if (currentGocaSubtype == haus.nightmare.lib3270j.graphics.GocaConstants.SF_OBJCNTL_SUB) {
gocaDecoder.processProcedureOrders(fullStream, 0, fullStream.length);
} else {
@@ -864,6 +873,7 @@ public class DataStreamProcessor {
notifyScreenUpdated();
} else { // SPAN_ONLY (0xC0)
gocaAccumulator.reset();
log.info(String.format("GOCA stream SPAN_ONLY: %d bytes", orderLen));
if (sfSubId == haus.nightmare.lib3270j.graphics.GocaConstants.SF_OBJCNTL_SUB) {
gocaDecoder.processProcedureOrders(data, orderOffset, orderLen);
} else {
@@ -20,7 +20,7 @@ public class GocaDecoder {
private int curX = 0;
private int curY = 0;
private int curColor = GocaConstants.GOCA_COLORS[0];
private int bgMix = 2; // BMX_OVERPAINT default
private int bgMix = 0; // BMX_DEFAULT (MIX_LEAVE / transparent background mix per GOCA spec)
private int bgColor = GocaConstants.GOCA_COLORS[8]; // Black
private int lineType = GocaConstants.LT_SOLID;
private int lineWidth = GocaConstants.LW_NORMAL;
@@ -28,6 +28,7 @@ public class GocaDecoder {
private int markerSize = 5;
private int markerColor = GocaConstants.GOCA_COLORS[0];
private int pattern = GocaConstants.PT_SOLID;
private int patternSet = 0;
private int fillColor = GocaConstants.GOCA_COLORS[0];
private int charDir = GocaConstants.CD_LR;
private double charAngle = 0.0;
@@ -99,7 +100,7 @@ public class GocaDecoder {
private final java.util.List<SegmentBounds> activeSegmentsInOrder = new java.util.ArrayList<>();
private int currentSegId = 0;
// Graphic Cursor (Light-Pen) state
// Graphics Cursor state
private boolean graphicsCursorActive = false;
private int graphicCursorX = 0;
private int graphicCursorY = 0;
@@ -110,6 +111,9 @@ public class GocaDecoder {
public void setProgramSymbolManager(ProgramSymbolManager psm) {
this.programSymbolManager = psm;
if (this.plane != null) {
this.plane.setProgramSymbolManager(psm);
}
}
public GraphicsPlane getGraphicsPlane() {
@@ -203,7 +207,7 @@ public class GocaDecoder {
public synchronized void resetAttributes() {
curColor = getColor(0);
bgMix = 2; // BMX_OVERPAINT
bgMix = 0; // BMX_DEFAULT (MIX_LEAVE / transparent background mix per GOCA spec)
bgColor = GocaConstants.GOCA_COLORS[8]; // Black
lineType = GocaConstants.LT_SOLID;
lineWidth = GocaConstants.LW_NORMAL;
@@ -211,6 +215,7 @@ public class GocaDecoder {
markerSize = 5;
markerColor = curColor;
pattern = GocaConstants.PT_SOLID;
patternSet = 0;
fillColor = curColor;
charDir = GocaConstants.CD_LR;
charAngle = 0.0;
@@ -232,30 +237,26 @@ public class GocaDecoder {
*
* IMPORTANT ARCHITECTURE & PARSING SAFETY NOTE:
* GOCA orders follow IBM GA23-0059 and Host On-Demand (HOD) architecture rules:
* 1. 1-byte standalone orders (NOP, ERASE, etc.) -> length 1.
* 2. Delimiter orders (GEAR, ENDSEGM, ENDPROLOGUE, GEIMG, GPOP) -> 1 or 2 bytes (with 0x00 trailing byte).
* 3. Fixed 1-byte operand orders (0x00..0x1F range: GSCOL, GSLT, GSLW, GSMS, GSMC, GSPS, GSBMX) -> 2 bytes.
* 4. Flexible 1-byte attribute orders (GBAR 0x68, GSCS 0x38, GSCD 0x3A, GSCC 0x3B, GSMP 0x39,
* 1. 1-byte standalone orders (NOP, ERASE, GEAR, ENDSEGM, ENDPROLOGUE, GEIMG, GPOP) -> length 1.
* 2. Fixed 1-byte operand orders (0x00..0x1F range: GSCOL, GSLT, GSLW, GSMS, GSMC, GSPS, GSBMX) -> 2 bytes.
* 3. Flexible 1-byte attribute orders (GBAR 0x68, GSCS 0x38, GSCD 0x3A, GSCC 0x3B, GSMP 0x39,
* GSPT 0x28, GSMT 0x29, GSMS_SET 0x3C) can be transmitted either as:
* - Short 2-byte form: [opcode] [value] (e.g. GBAR with flags 0x80 -> '68 80')
* - Self-defining 3-byte form: [opcode] [length=0x01] [value] (e.g. '68 01 80')
* CRITICAL: NEVER allow 1-byte attribute orders (like GBAR 0x68 or GSCC 0x3B) to fall through to the
* variable-length formula `(data[idx + 1] & 0xFF) + 2`. If GBAR flags (e.g. 0x80 for boundary) are read as
* a length field, the decoder will skip 130 bytes, corrupting and skipping all subsequent drawing orders!
* 5. Self-defining orders with multi-byte payloads (GLINE 0xC1, GARC 0xC6, GCHST 0xC3, GRLINE 0xE1, etc.)
* 4. Self-defining orders with multi-byte payloads (GLINE 0xC1, GARC 0xC6, GCHST 0xC3, GRLINE 0xE1, etc.)
* have a 1-byte length byte at data[idx + 1], making total length = payloadLen + 2.
*/
private int getOrderLength(byte[] data, int idx, int end) {
int order = data[idx] & 0xFF;
if (order == GocaConstants.G_NOP1 || order == 0xFF || order == 0x00) {
if (order == GocaConstants.G_NOP1 || order == 0xFF || order == 0x00 ||
order == GocaConstants.G_GEAR || order == GocaConstants.G_ENDSEGM ||
order == GocaConstants.G_ENDPROLOGUE || order == GocaConstants.G_GEIMG ||
order == GocaConstants.G_GPOP || order == GocaConstants.G_GERASE) {
return 1;
}
if (order == GocaConstants.G_GEAR ||
order == GocaConstants.G_ENDSEGM || order == GocaConstants.G_ENDPROLOGUE ||
order == GocaConstants.G_GEIMG || order == GocaConstants.G_GPOP ||
order == GocaConstants.G_GERASE) {
return (idx + 1 < end && data[idx + 1] == 0x00) ? 2 : 1;
}
if (idx + 1 >= end) {
return -1;
}
@@ -399,23 +400,30 @@ public class GocaDecoder {
break;
}
case GocaConstants.G_BEGSEGM: { // Begin Segment (0x70)
int segId = 0;
int flag0 = 0;
int flag1 = 0;
if (idx + 5 < end) {
int segId = ((inputData[idx + 2] & 0xFF) << 24) |
((inputData[idx + 3] & 0xFF) << 16) |
((inputData[idx + 4] & 0xFF) << 8) |
(inputData[idx + 5] & 0xFF);
segId = ((inputData[idx + 2] & 0xFF) << 24) |
((inputData[idx + 3] & 0xFF) << 16) |
((inputData[idx + 4] & 0xFF) << 8) |
(inputData[idx + 5] & 0xFF);
currentSegId = segId;
SegmentBounds sb = segmentBoundsMap.computeIfAbsent(segId, SegmentBounds::new);
activeSegmentsInOrder.remove(sb);
activeSegmentsInOrder.add(sb);
}
if (idx + 7 < end && (inputData[idx + 7] & 0x06) == 0) {
if (idx + 6 < end) flag0 = inputData[idx + 6] & 0xFF;
if (idx + 7 < end) flag1 = inputData[idx + 7] & 0xFF;
logger.info(String.format("GOCA BEGSEGM: segId=%d flag0=0x%02x flag1=0x%02x callDepth=%d", segId, flag0, flag1, callDepth));
if (idx + 7 < end && (flag1 & 0x06) == 0) {
resetAttributes();
}
idx += orderLen;
break;
}
case GocaConstants.G_ENDSEGM: { // End Segment (0x71)
logger.info(String.format("GOCA ENDSEGM: segId=%d", currentSegId));
if (currentSegId != 0) {
SegmentBounds sb = segmentBoundsMap.get(currentSegId);
if (sb != null) {
@@ -486,6 +494,7 @@ public class GocaDecoder {
} else {
targetSegId = -1;
}
logger.info(String.format("GOCA GCALL: targetSegId=%d callDepth=%d", targetSegId, callDepth));
if (targetSegId != -1) {
byte[] targetSeg = segmentStore.get(targetSegId);
if (targetSeg != null) {
@@ -498,6 +507,8 @@ public class GocaDecoder {
decodeStreamDirect(targetSeg, 0, targetSeg.length);
callDepth--;
currentSegId = savedSegId;
} else {
logger.warning("GOCA GCALL: target segment not found in store: " + targetSegId);
}
}
}
@@ -530,7 +541,9 @@ public class GocaDecoder {
case GocaConstants.G_GSCOL: { // Set Color (0x0A)
int colIdx = inputData[idx + 1] & 0xFF;
curColor = getColor(colIdx);
fillColor = curColor;
if (!inArea) {
fillColor = curColor;
}
idx += orderLen;
break;
}
@@ -538,7 +551,9 @@ public class GocaDecoder {
if (payloadLen >= 2 && idx + 3 < end) {
int colIdx = inputData[idx + 3] & 0xFF;
curColor = getColor(colIdx);
fillColor = curColor;
if (!inArea) {
fillColor = curColor;
}
}
idx += orderLen;
break;
@@ -570,12 +585,14 @@ public class GocaDecoder {
break;
}
case GocaConstants.G_GSPS: { // Set Pattern Set (0x08)
pattern = inputData[idx + 1] & 0xFF;
patternSet = inputData[idx + 1] & 0xFF;
logger.info("GOCA GSPS: patternSet=" + patternSet);
idx += orderLen;
break;
}
case GocaConstants.G_GSPT: { // Set Pattern Symbol (0x28)
pattern = (orderLen == 3) ? (inputData[idx + 2] & 0xFF) : (inputData[idx + 1] & 0xFF);
logger.info("GOCA GSPT: pattern=" + pattern);
idx += orderLen;
break;
}
@@ -605,12 +622,14 @@ public class GocaDecoder {
}
case GocaConstants.G_GSBMX: { // Set Background Mix (0x0D)
bgMix = inputData[idx + 1] & 0xFF;
logger.info("GOCA GSBMX: bgMix=" + bgMix);
idx += orderLen;
break;
}
case GocaConstants.G_GBAR: { // Begin Area (0x68)
int flags = (orderLen == 3) ? (inputData[idx + 2] & 0xFF) : (inputData[idx + 1] & 0xFF);
boolean drawBoundary = (flags & 0x80) != 0 || (flags & 0x40) != 0 || (flags == 0);
boolean drawBoundary = (flags & 0x80) != 0;
logger.info(String.format("GOCA GBAR: flags=0x%02x drawBoundary=%b", flags, drawBoundary));
beginArea(drawBoundary);
idx += orderLen;
break;
@@ -837,6 +856,8 @@ public class GocaDecoder {
this.areaPointsY.clear();
this.areaPolygons.clear();
this.currentPolyPts = 0;
logger.info(String.format("GOCA beginArea: drawBoundary=%b fillColor=0x%08x patternSet=%d pattern=%d",
drawBoundary, fillColor, patternSet, pattern));
}
private void endArea() {
@@ -846,6 +867,7 @@ public class GocaDecoder {
currentPolyPts = 0;
}
if (areaPointsX.size() < 3 || areaPolygons.isEmpty()) {
logger.info(String.format("GOCA endArea: dropped (pts=%d, polys=%d)", areaPointsX.size(), areaPolygons.size()));
inArea = false;
areaPointsX.clear();
areaPointsY.clear();
@@ -867,7 +889,10 @@ public class GocaDecoder {
polyCounts[i] = areaPolygons.get(i);
}
plane.fillArea(px, py, n, polyCounts, numPolys, fillColor,
logger.info(String.format("GOCA endArea: fillArea pts=%d polys=%d fillColor=0x%08x patternSet=%d pattern=%d drawBoundary=%b boundaryColor=0x%08x bgMix=%d",
n, numPolys, fillColor, patternSet, pattern, areaDrawBoundary, curColor, bgMix));
plane.fillArea(px, py, n, polyCounts, numPolys, fillColor, patternSet,
pattern, areaDrawBoundary, curColor, lineType, lineWidth, bgMix, bgColor);
inArea = false;
areaPointsX.clear();
@@ -1108,6 +1133,22 @@ public class GocaDecoder {
trackPoint(centerX - (int) Math.round(semiAxis1), centerY - (int) Math.round(semiAxis2));
trackPoint(centerX + (int) Math.round(semiAxis1), centerY + (int) Math.round(semiAxis2));
if (inArea) {
int steps = Math.max(8, (int) Math.round(Math.abs(sweepAngleDeg) / 15.0));
double startRad = Math.toRadians(startAngleDeg);
double sweepRad = Math.toRadians(sweepAngleDeg);
for (int i = 0; i <= steps; i++) {
double a = startRad + (sweepRad * i) / steps;
int ax = centerX + (int) Math.round(Math.cos(a) * semiAxis1);
int ay = centerY + (int) Math.round(Math.sin(a) * semiAxis2);
if (i == 0 && currentPolyPts == 0) {
addAreaLineStart(ax, ay);
} else {
addAreaPoint(ax, ay);
}
}
}
plane.drawArc(cx, cy, rx, ry, startAngleDeg, sweepAngleDeg,
curColor, lineType, lineWidth, isFull);
@@ -1125,6 +1166,13 @@ public class GocaDecoder {
if (fromCurPos) {
ptsX.add(plane.mapXDouble(curX));
ptsY.add(plane.mapYDouble(curY));
if (inArea) {
if (currentPolyPts == 0) {
addAreaLineStart(curX, curY);
} else {
addAreaPoint(curX, curY);
}
}
}
while (pos + 4 <= end) {
@@ -1132,6 +1180,13 @@ public class GocaDecoder {
int y = readCoord(data, pos + 2);
ptsX.add(plane.mapXDouble(x));
ptsY.add(plane.mapYDouble(y));
if (inArea) {
if (ptsX.size() == 1 && currentPolyPts == 0) {
addAreaLineStart(x, y);
} else {
addAreaPoint(x, y);
}
}
curX = x;
curY = y;
pos += 4;
@@ -44,6 +44,15 @@ public class GraphicsPlane {
private int screenCols = 80;
private int screenRows = 24;
private ProgramSymbolManager programSymbolManager;
public void setProgramSymbolManager(ProgramSymbolManager psm) {
this.programSymbolManager = psm;
}
public ProgramSymbolManager getProgramSymbolManager() {
return programSymbolManager;
}
public GraphicsPlane(int width, int height) {
this.canvasWidth = Math.max(1, width);
@@ -338,7 +347,7 @@ public class GraphicsPlane {
}
private void drawStyledLine(int x1, int y1, int x2, int y2, int colorArgb, int lineType, int lineWidth) {
int color = (colorArgb != 0) ? colorArgb : 0xFFFFFFFF;
int color = (colorArgb != 0) ? colorArgb : GocaConstants.GOCA_COLORS[0];
int thickness = (lineWidth == GocaConstants.LW_THICK) ? 2 : 1;
int dx = Math.abs(x2 - x1);
@@ -517,12 +526,23 @@ public class GraphicsPlane {
int fillColorArgb, int pattern,
boolean drawBoundary, int boundaryColorArgb, int lineType, int lineWidth,
int bgMix, int bgColorArgb) {
fillArea(px, py, numPoints, polyCounts, numPolys, fillColorArgb, 0, pattern, drawBoundary, boundaryColorArgb, lineType, lineWidth, bgMix, bgColorArgb);
}
/**
* Fills an area with symbol-set-aware and pattern-symbol-aware rasterization.
*/
public synchronized void fillArea(int[] px, int[] py, int numPoints, int[] polyCounts, int numPolys,
int fillColorArgb, int patternSet, int pattern,
boolean drawBoundary, int boundaryColorArgb, int lineType, int lineWidth,
int bgMix, int bgColorArgb) {
if (px == null || py == null || numPoints < 3) return;
int fill = (fillColorArgb != 0) ? fillColorArgb : 0xFFFFFFFF;
int fill = (fillColorArgb != 0) ? fillColorArgb : GocaConstants.GOCA_COLORS[0];
int bg = bgColorArgb;
if (pattern != GocaConstants.PT_EMPTY) {
boolean isTransparentBlack = (fill == GocaConstants.GOCA_COLORS[8] || (fill & 0x00FFFFFF) == 0) && (bgMix != GocaConstants.MIX_OVER);
if (pattern != GocaConstants.PT_EMPTY && (pattern != 0 || !drawBoundary) && !isTransparentBlack) {
// Find polygon vertical bounds across all points
int minY = py[0];
int maxY = py[0];
@@ -534,7 +554,18 @@ public class GraphicsPlane {
maxY = Math.min(canvasHeight - 1, maxY);
List<Integer> nodeX = new ArrayList<>();
byte[] patRows = (pattern >= 0 && pattern < PATTERN_DATA.length) ? PATTERN_DATA[pattern] : PATTERN_DATA[0];
byte[] patRows = null;
ProgramSymbolSet.SymbolSlot psSlot = null;
if (patternSet >= 0x40 && programSymbolManager != null) {
psSlot = programSymbolManager.getSymbol(patternSet, pattern);
}
if (psSlot == null) {
if (pattern >= 0 && pattern < PATTERN_DATA.length) {
patRows = PATTERN_DATA[pattern];
} else {
patRows = PATTERN_DATA[0];
}
}
for (int y = minY; y <= maxY; y++) {
nodeX.clear();
@@ -567,13 +598,26 @@ public class GraphicsPlane {
int rightX = Math.min(canvasWidth - 1, nodeX.get(i + 1));
for (int x = leftX; x <= rightX; x++) {
if (pattern == GocaConstants.PT_SOLID || pattern == 0 || pattern >= 16) {
if (psSlot != null) {
int psW = psSlot.getWidth();
int psH = psSlot.getHeight();
int psX = (psW > 0) ? (x % psW) : 0;
int psY = (psH > 0) ? (y % psH) : 0;
byte[] psPix = psSlot.getPixelData();
int pIdx = psY * psW + psX;
boolean bit = (psPix != null && pIdx < psPix.length && psPix[pIdx] != 0);
if (bit) {
setPixel(x, y, fill);
} else if (bgMix == GocaConstants.MIX_OVER) {
setPixel(x, y, bg);
}
} else if (pattern == GocaConstants.PT_SOLID || pattern == 16) {
setPixel(x, y, fill);
} else {
int b = patRows[y & 7] & 0xFF;
if (((b >> (7 - (x & 7))) & 1) != 0) {
setPixel(x, y, fill);
} else if (bgMix != 0) { // BMX_OVERPAINT (opaque background)
} else if (bgMix == GocaConstants.MIX_OVER) { // BMX_OVERPAINT (opaque background)
setPixel(x, y, bg);
}
}
@@ -610,7 +654,7 @@ public class GraphicsPlane {
* Draws a GOCA marker symbol (+, x, diamond, square, star, dot, circle) with sub-pixel precision.
*/
public synchronized void drawMarker(double x, double y, int markerType, int size, int colorArgb) {
int color = (colorArgb != 0) ? colorArgb : 0xFFFFFFFF;
int color = (colorArgb != 0) ? colorArgb : GocaConstants.GOCA_COLORS[0];
double s = Math.max(3.0, size > 0 ? (double) size : 5.0);
switch (markerType) {
@@ -731,7 +775,7 @@ public class GraphicsPlane {
public synchronized void drawVectorText(double x, double y, String text, int colorArgb,
double cellWidth, double cellHeight, int dir, double angle) {
if (text == null || text.isEmpty()) return;
int color = (colorArgb != 0) ? colorArgb : 0xFFFFFFFF;
int color = (colorArgb != 0) ? colorArgb : GocaConstants.GOCA_COLORS[0];
double curX = x;
double curY = y;
@@ -822,7 +866,7 @@ public class GraphicsPlane {
*/
public synchronized void drawImage(int x, int y, int width, int height, byte[] imageData, int fgColorArgb) {
if (imageData == null || width <= 0 || height <= 0) return;
int fgColor = (fgColorArgb != 0) ? fgColorArgb : 0xFFFFFFFF;
int fgColor = (fgColorArgb != 0) ? fgColorArgb : GocaConstants.GOCA_COLORS[0];
int bytesPerRow = (width + 7) / 8;
for (int row = 0; row < height; row++) {