From 2208c843925b245e4490b7d8f5b9ea2b103c6b59 Mon Sep 17 00:00:00 2001 From: brachy84 Date: Fri, 28 Nov 2025 08:50:37 +0100 Subject: [PATCH 01/50] optimise adaptable drawing (cherry picked from commit 848d9e6035f146e63985fc2cc2147a9528e3c2ee) --- .../drawable/AdaptableUITexture.java | 150 +++++++++--------- .../modularui/drawable/GuiDraw.java | 62 ++++---- 2 files changed, 105 insertions(+), 107 deletions(-) diff --git a/src/main/java/com/cleanroommc/modularui/drawable/AdaptableUITexture.java b/src/main/java/com/cleanroommc/modularui/drawable/AdaptableUITexture.java index 91cf5db1d..274a09679 100644 --- a/src/main/java/com/cleanroommc/modularui/drawable/AdaptableUITexture.java +++ b/src/main/java/com/cleanroommc/modularui/drawable/AdaptableUITexture.java @@ -64,42 +64,44 @@ public void drawStretched(float x, float y, float width, float height) { float x1 = x + width, y1 = y + height; float uInnerStart = this.u0 + uBl, vInnerStart = this.v0 + vBt, uInnerEnd = this.u1 - uBr, vInnerEnd = this.v1 - vBb; - if ((this.bl > 0 || this.br > 0) && this.bt <= 0 && this.bb <= 0) { - // left border - GuiDraw.drawTexture(x, y, x + this.bl, y1, this.u0, this.v0, uInnerStart, this.v1); - // right border - GuiDraw.drawTexture(x1 - this.br, y, x1, y1, uInnerEnd, this.v0, this.u1, this.v1); - // center - GuiDraw.drawTexture(x + this.bl, y, x1 - this.br, y1, uInnerStart, this.v0, uInnerEnd, this.v1); - } else if (this.bl <= 0 && this.br <= 0) { - // top border - GuiDraw.drawTexture(x, y, x1, y + this.bt, this.u0, this.v0, this.u1, vInnerStart); - // bottom border - GuiDraw.drawTexture(x, y1 - this.bb, x1, y1, this.u0, vInnerEnd, this.u1, this.v1); - // center - GuiDraw.drawTexture(x, y + this.bt, x1, y1 - this.bb, this.u0, vInnerStart, this.u1, vInnerEnd); - } else { - // top left corner - GuiDraw.drawTexture(x, y, x + this.bl, y + this.bt, this.u0, this.v0, uInnerStart, vInnerStart); - // top right corner - GuiDraw.drawTexture(x1 - this.br, y, x1, y + this.bt, uInnerEnd, this.v0, this.u1, vInnerStart); - // bottom left corner - GuiDraw.drawTexture(x, y1 - this.bb, x + this.bl, y1, this.u0, vInnerEnd, uInnerStart, this.v1); - // bottom right corner - GuiDraw.drawTexture(x1 - this.br, y1 - this.bb, x1, y1, uInnerEnd, vInnerEnd, this.u1, this.v1); - - // left border - GuiDraw.drawTexture(x, y + this.bt, x + this.bl, y1 - this.bb, this.u0, vInnerStart, uInnerStart, vInnerEnd); - // top border - GuiDraw.drawTexture(x + this.bl, y, x1 - this.br, y + this.bt, uInnerStart, this.v0, uInnerEnd, vInnerStart); - // right border - GuiDraw.drawTexture(x1 - this.br, y + this.bt, x1, y1 - this.bb, uInnerEnd, vInnerStart, this.u1, vInnerEnd); - // bottom border - GuiDraw.drawTexture(x + this.bl, y1 - this.bb, x1 - this.br, y1, uInnerStart, vInnerEnd, uInnerEnd, this.v1); - - // center - GuiDraw.drawTexture(x + this.bl, y + this.bt, x1 - this.br, y1 - this.bb, uInnerStart, vInnerStart, uInnerEnd, vInnerEnd); - } + Platform.startDrawing(Platform.DrawMode.QUADS, Platform.VertexFormat.POS_TEX, buffer -> { + if ((this.bl > 0 || this.br > 0) && this.bt <= 0 && this.bb <= 0) { + // left border + GuiDraw.drawTexture(buffer, x, y, x + this.bl, y1, this.u0, this.v0, uInnerStart, this.v1, 0); + // right border + GuiDraw.drawTexture(buffer, x1 - this.br, y, x1, y1, uInnerEnd, this.v0, this.u1, this.v1, 0); + // center + GuiDraw.drawTexture(buffer, x + this.bl, y, x1 - this.br, y1, uInnerStart, this.v0, uInnerEnd, this.v1, 0); + } else if (this.bl <= 0 && this.br <= 0) { + // top border + GuiDraw.drawTexture(buffer, x, y, x1, y + this.bt, this.u0, this.v0, this.u1, vInnerStart, 0); + // bottom border + GuiDraw.drawTexture(buffer, x, y1 - this.bb, x1, y1, this.u0, vInnerEnd, this.u1, this.v1, 0); + // center + GuiDraw.drawTexture(buffer, x, y + this.bt, x1, y1 - this.bb, this.u0, vInnerStart, this.u1, vInnerEnd, 0); + } else { + // top left corner + GuiDraw.drawTexture(buffer, x, y, x + this.bl, y + this.bt, this.u0, this.v0, uInnerStart, vInnerStart, 0); + // top right corner + GuiDraw.drawTexture(buffer, x1 - this.br, y, x1, y + this.bt, uInnerEnd, this.v0, this.u1, vInnerStart, 0); + // bottom left corner + GuiDraw.drawTexture(buffer, x, y1 - this.bb, x + this.bl, y1, this.u0, vInnerEnd, uInnerStart, this.v1, 0); + // bottom right corner + GuiDraw.drawTexture(buffer, x1 - this.br, y1 - this.bb, x1, y1, uInnerEnd, vInnerEnd, this.u1, this.v1, 0); + + // left border + GuiDraw.drawTexture(buffer, x, y + this.bt, x + this.bl, y1 - this.bb, this.u0, vInnerStart, uInnerStart, vInnerEnd, 0); + // top border + GuiDraw.drawTexture(buffer, x + this.bl, y, x1 - this.br, y + this.bt, uInnerStart, this.v0, uInnerEnd, vInnerStart, 0); + // right border + GuiDraw.drawTexture(buffer, x1 - this.br, y + this.bt, x1, y1 - this.bb, uInnerEnd, vInnerStart, this.u1, vInnerEnd, 0); + // bottom border + GuiDraw.drawTexture(buffer, x + this.bl, y1 - this.bb, x1 - this.br, y1, uInnerStart, vInnerEnd, uInnerEnd, this.v1, 0); + + // center + GuiDraw.drawTexture(buffer, x + this.bl, y + this.bt, x1 - this.br, y1 - this.bb, uInnerStart, vInnerStart, uInnerEnd, vInnerEnd, 0); + } + }); GlStateManager.disableBlend(); GlStateManager.enableAlpha(); } @@ -120,42 +122,46 @@ public void drawTiled(float x, float y, float width, float height) { int tw = (int) (this.imageWidth * (this.u1 - this.u0)); int th = (int) (this.imageHeight * (this.v1 - this.v0)); - if ((this.bl > 0 || this.br > 0) && this.bt <= 0 && this.bb <= 0) { - // left border - GuiDraw.drawTiledTexture(x, y, this.bl, height, this.u0, this.v0, uInnerStart, this.v1, this.bl, th, 0); - // right border - GuiDraw.drawTiledTexture(x1 - this.br, y, this.br, height, uInnerEnd, this.v0, this.u1, this.v1, this.br, th, 0); - // center - GuiDraw.drawTiledTexture(x + this.bl, y, width - this.bl - this.br, height, uInnerStart, this.v0, uInnerEnd, this.v1, tw - this.bl - this.br, th, 0); - } else if (this.bl <= 0 && this.br <= 0) { - // top border - GuiDraw.drawTiledTexture(x, y, width, this.bt, this.u0, this.v0, this.u1, vInnerStart, tw, this.bt, 0); - // bottom border - GuiDraw.drawTiledTexture(x, y1 - this.bb, width, this.bb, this.u0, vInnerEnd, this.u1, this.v1, tw, this.bb, 0); - // center - GuiDraw.drawTiledTexture(x, y + this.bt, width, height - this.bt - this.bb, this.u0, vInnerStart, this.u1, vInnerEnd, tw, th - this.bt - this.bb, 0); - } else { - // top left corner - GuiDraw.drawTiledTexture(x, y, this.bl, this.bt, this.u0, this.v0, uInnerStart, vInnerStart, this.bl, this.bt, 0); - // top right corner - GuiDraw.drawTiledTexture(x1 - this.br, y, this.br, this.bt, uInnerEnd, this.v0, this.u1, vInnerStart, this.br, this.bt, 0); - // bottom left corner - GuiDraw.drawTiledTexture(x, y1 - this.bb, this.bl, this.bb, this.u0, vInnerEnd, uInnerStart, this.v1, this.bl, this.bb, 0); - // bottom right corner - GuiDraw.drawTiledTexture(x1 - this.br, y1 - this.bb, this.br, this.bb, uInnerEnd, vInnerEnd, this.u1, this.v1, this.br, this.bb, 0); - - // left border - GuiDraw.drawTiledTexture(x, y + this.bt, this.bl, height - this.bt - this.bb, this.u0, vInnerStart, uInnerStart, vInnerEnd, this.bl, th - this.bt - this.bb, 0); - // top border - GuiDraw.drawTiledTexture(x + this.bl, y, width - this.bl - this.br, this.bt, uInnerStart, this.v0, uInnerEnd, vInnerStart, tw - this.bl - this.bb, this.bt, 0); - // right border - GuiDraw.drawTiledTexture(x1 - this.br, y + this.bt, this.br, height - this.bt - this.bb, uInnerEnd, vInnerStart, this.u1, vInnerEnd, this.br, th - this.bt - this.bb, 0); - // bottom border - GuiDraw.drawTiledTexture(x + this.bl, y1 - this.bb, width - this.bl - this.br, this.bb, uInnerStart, vInnerEnd, uInnerEnd, this.v1, tw - this.bl - this.br, this.bb, 0); - - // center - GuiDraw.drawTiledTexture(x + this.bl, y + this.bt, width - this.bl - this.br, height - this.bt - this.bb, uInnerStart, vInnerStart, uInnerEnd, vInnerEnd, tw - this.bl - this.br, th - this.bt - this.bb, 0); - } + Platform.startDrawing(Platform.DrawMode.QUADS, Platform.VertexFormat.POS_TEX, buffer -> { + if ((this.bl > 0 || this.br > 0) && this.bt <= 0 && this.bb <= 0) { + // left border + GuiDraw.drawTiledTexture(buffer, x, y, this.bl, height, this.u0, this.v0, uInnerStart, this.v1, this.bl, th, 0); + // right border + GuiDraw.drawTiledTexture(buffer, x1 - this.br, y, this.br, height, uInnerEnd, this.v0, this.u1, this.v1, this.br, th, 0); + // center + GuiDraw.drawTiledTexture(buffer, x + this.bl, y, width - this.bl - this.br, height, uInnerStart, this.v0, uInnerEnd, this.v1, tw - this.bl - this.br, th, 0); + } else if (this.bl <= 0 && this.br <= 0) { + // top border + GuiDraw.drawTiledTexture(buffer, x, y, width, this.bt, this.u0, this.v0, this.u1, vInnerStart, tw, this.bt, 0); + // bottom border + GuiDraw.drawTiledTexture(buffer, x, y1 - this.bb, width, this.bb, this.u0, vInnerEnd, this.u1, this.v1, tw, this.bb, 0); + // center + GuiDraw.drawTiledTexture(buffer, x, y + this.bt, width, height - this.bt - this.bb, this.u0, vInnerStart, this.u1, vInnerEnd, tw, th - this.bt - this.bb, 0); + } else { + // corners don't need to be tiled + // they are drawn at their size + // top left corner + GuiDraw.drawTexture(buffer, x, y, x + this.bl, y + this.bt, this.u0, this.v0, uInnerStart, vInnerStart, 0); + // top right corner + GuiDraw.drawTexture(buffer, x1 - this.br, y, x1, y + this.bt, uInnerEnd, this.v0, this.u1, vInnerStart, 0); + // bottom left corner + GuiDraw.drawTexture(buffer, x, y1 - this.bb, x + this.bl, y1, this.u0, vInnerEnd, uInnerStart, this.v1, 0); + // bottom right corner + GuiDraw.drawTexture(buffer, x1 - this.br, y1 - this.bb, x1, y1, uInnerEnd, vInnerEnd, this.u1, this.v1, 0); + + // left border + GuiDraw.drawTiledTexture(buffer, x, y + this.bt, this.bl, height - this.bt - this.bb, this.u0, vInnerStart, uInnerStart, vInnerEnd, this.bl, th - this.bt - this.bb, 0); + // top border + GuiDraw.drawTiledTexture(buffer, x + this.bl, y, width - this.bl - this.br, this.bt, uInnerStart, this.v0, uInnerEnd, vInnerStart, tw - this.bl - this.bb, this.bt, 0); + // right border + GuiDraw.drawTiledTexture(buffer, x1 - this.br, y + this.bt, this.br, height - this.bt - this.bb, uInnerEnd, vInnerStart, this.u1, vInnerEnd, this.br, th - this.bt - this.bb, 0); + // bottom border + GuiDraw.drawTiledTexture(buffer, x + this.bl, y1 - this.bb, width - this.bl - this.br, this.bb, uInnerStart, vInnerEnd, uInnerEnd, this.v1, tw - this.bl - this.br, this.bb, 0); + + // center + GuiDraw.drawTiledTexture(buffer, x + this.bl, y + this.bt, width - this.bl - this.br, height - this.bt - this.bb, uInnerStart, vInnerStart, uInnerEnd, vInnerEnd, tw - this.bl - this.br, th - this.bt - this.bb, 0); + } + }); GlStateManager.disableBlend(); GlStateManager.enableAlpha(); } diff --git a/src/main/java/com/cleanroommc/modularui/drawable/GuiDraw.java b/src/main/java/com/cleanroommc/modularui/drawable/GuiDraw.java index b345b0a12..b5a50e74f 100644 --- a/src/main/java/com/cleanroommc/modularui/drawable/GuiDraw.java +++ b/src/main/java/com/cleanroommc/modularui/drawable/GuiDraw.java @@ -252,23 +252,10 @@ public static void drawTiledTexture(ResourceLocation location, float x, float y, drawTiledTexture(x, y, w, h, u, v, tileW, tileH, tw, th, z); } - public static void drawTiledTexture(float x, float y, float w, float h, int u, int v, int tileW, int tileH, int tw, int th, float z) { - int countX = (((int) w - 1) / tileW) + 1; - int countY = (((int) h - 1) / tileH) + 1; - float fillerX = w - (countX - 1) * tileW; - float fillerY = h - (countY - 1) * tileH; - Platform.startDrawing(Platform.DrawMode.QUADS, Platform.VertexFormat.POS_TEX, bufferBuilder -> { - for (int i = 0, c = countX * countY; i < c; i++) { - int ix = i % countX; - int iy = i / countX; - float xx = x + ix * tileW; - float yy = y + iy * tileH; - float xw = ix == countX - 1 ? fillerX : tileW; - float yh = iy == countY - 1 ? fillerY : tileH; - - drawTexture(bufferBuilder, xx, yy, u, v, xw, yh, tw, th, z); - } - }); + public static void drawTiledTexture(float x, float y, float w, float h, int u, int v, int tileW, int tileH, int textureW, int textureH, float z) { + float tw = 1f / textureW; + float th = 1f / textureH; + drawTiledTexture(x, y, w, h, u * tw, v * th, (u + w) * tw, (v + h) * th, textureW, textureH, z); } public static void drawTiledTexture(ResourceLocation location, float x, float y, float w, float h, float u0, float v0, float u1, float v1, int textureWidth, int textureHeight, float z) { @@ -277,31 +264,36 @@ public static void drawTiledTexture(ResourceLocation location, float x, float y, } public static void drawTiledTexture(float x, float y, float w, float h, float u0, float v0, float u1, float v1, int tileWidth, int tileHeight, float z) { + Platform.startDrawing(Platform.DrawMode.QUADS, Platform.VertexFormat.POS_TEX, bufferBuilder -> { + drawTiledTexture(bufferBuilder, x, y, w, h, u0, v0, u1, v1, tileWidth, tileHeight, z); + }); + } + + public static void drawTiledTexture(BufferBuilder bufferBuilder, float x, float y, float w, float h, float u0, float v0, float u1, float v1, int tileWidth, int tileHeight, float z) { int countX = (((int) w - 1) / tileWidth) + 1; int countY = (((int) h - 1) / tileHeight) + 1; float fillerX = w - (countX - 1) * tileWidth; float fillerY = h - (countY - 1) * tileHeight; float fillerU = u0 + (u1 - u0) * fillerX / tileWidth; float fillerV = v0 + (v1 - v0) * fillerY / tileHeight; - Platform.startDrawing(Platform.DrawMode.QUADS, Platform.VertexFormat.POS_TEX, bufferBuilder -> { - for (int i = 0, c = countX * countY; i < c; i++) { - int ix = i % countX; - int iy = i / countX; - float xx = x + ix * tileWidth; - float yy = y + iy * tileHeight; - float xw = tileWidth, yh = tileHeight, uEnd = u1, vEnd = v1; - if (ix == countX - 1) { - xw = fillerX; - uEnd = fillerU; - } - if (iy == countY - 1) { - yh = fillerY; - vEnd = fillerV; - } - - drawTexture(bufferBuilder, xx, yy, xx + xw, yy + yh, u0, v0, uEnd, vEnd, z); + + for (int i = 0, c = countX * countY; i < c; i++) { + int ix = i % countX; + int iy = i / countX; + float xx = x + ix * tileWidth; + float yy = y + iy * tileHeight; + float xw = tileWidth, yh = tileHeight, uEnd = u1, vEnd = v1; + if (ix == countX - 1) { + xw = fillerX; + uEnd = fillerU; } - }); + if (iy == countY - 1) { + yh = fillerY; + vEnd = fillerV; + } + + drawTexture(bufferBuilder, xx, yy, xx + xw, yy + yh, u0, v0, uEnd, vEnd, z); + } } public static void drawItem(ItemStack item, int x, int y, float width, float height, int z) { From 3b50bd18f2a731086bf507b1d3687169fc7ef019 Mon Sep 17 00:00:00 2001 From: brachy84 Date: Sat, 29 Nov 2025 15:49:21 +0100 Subject: [PATCH 02/50] fix sync value update listener only being called on sync (cherry picked from commit 7d98df2e154efd33a7a4b4abd882dde71cae50de) --- .../cleanroommc/modularui/value/sync/BooleanSyncValue.java | 1 + .../com/cleanroommc/modularui/value/sync/ByteSyncValue.java | 1 + .../cleanroommc/modularui/value/sync/DoubleSyncValue.java | 1 + .../com/cleanroommc/modularui/value/sync/EnumSyncValue.java | 1 + .../cleanroommc/modularui/value/sync/FloatSyncValue.java | 1 + .../modularui/value/sync/GenericCollectionSyncHandler.java | 1 + .../modularui/value/sync/GenericMapSyncHandler.java | 1 + .../cleanroommc/modularui/value/sync/GenericSyncValue.java | 1 + .../com/cleanroommc/modularui/value/sync/IntSyncValue.java | 1 + .../com/cleanroommc/modularui/value/sync/LongSyncValue.java | 1 + .../cleanroommc/modularui/value/sync/StringSyncValue.java | 1 + .../cleanroommc/modularui/value/sync/ValueSyncHandler.java | 6 ++++-- 12 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/cleanroommc/modularui/value/sync/BooleanSyncValue.java b/src/main/java/com/cleanroommc/modularui/value/sync/BooleanSyncValue.java index abff0e984..d17898342 100644 --- a/src/main/java/com/cleanroommc/modularui/value/sync/BooleanSyncValue.java +++ b/src/main/java/com/cleanroommc/modularui/value/sync/BooleanSyncValue.java @@ -70,6 +70,7 @@ public void setValue(@NotNull Boolean value, boolean setSource, boolean sync) { @Override public void setBoolValue(boolean value, boolean setSource, boolean sync) { this.cache = value; + onValueChanged(); if (setSource && this.setter != null) { this.setter.accept(value); } diff --git a/src/main/java/com/cleanroommc/modularui/value/sync/ByteSyncValue.java b/src/main/java/com/cleanroommc/modularui/value/sync/ByteSyncValue.java index 4abc9f58d..f01067706 100644 --- a/src/main/java/com/cleanroommc/modularui/value/sync/ByteSyncValue.java +++ b/src/main/java/com/cleanroommc/modularui/value/sync/ByteSyncValue.java @@ -86,6 +86,7 @@ public Byte getValue() { @Override public void setByteValue(byte value, boolean setSource, boolean sync) { this.cache = value; + onValueChanged(); if (setSource && this.setter != null) { this.setter.setByte(value); } diff --git a/src/main/java/com/cleanroommc/modularui/value/sync/DoubleSyncValue.java b/src/main/java/com/cleanroommc/modularui/value/sync/DoubleSyncValue.java index 70fdcdb6c..61a7fa38d 100644 --- a/src/main/java/com/cleanroommc/modularui/value/sync/DoubleSyncValue.java +++ b/src/main/java/com/cleanroommc/modularui/value/sync/DoubleSyncValue.java @@ -71,6 +71,7 @@ public void setValue(@NotNull Double value, boolean setSource, boolean sync) { @Override public void setDoubleValue(double value, boolean setSource, boolean sync) { this.cache = value; + onValueChanged(); if (setSource && this.setter != null) { this.setter.accept(value); } diff --git a/src/main/java/com/cleanroommc/modularui/value/sync/EnumSyncValue.java b/src/main/java/com/cleanroommc/modularui/value/sync/EnumSyncValue.java index f0793b2b9..c47bcae4d 100644 --- a/src/main/java/com/cleanroommc/modularui/value/sync/EnumSyncValue.java +++ b/src/main/java/com/cleanroommc/modularui/value/sync/EnumSyncValue.java @@ -69,6 +69,7 @@ public T getValue() { @Override public void setValue(T value, boolean setSource, boolean sync) { this.cache = value; + onValueChanged(); if (setSource && this.setter != null) { this.setter.accept(value); } diff --git a/src/main/java/com/cleanroommc/modularui/value/sync/FloatSyncValue.java b/src/main/java/com/cleanroommc/modularui/value/sync/FloatSyncValue.java index dd81335d7..8dd09ef36 100644 --- a/src/main/java/com/cleanroommc/modularui/value/sync/FloatSyncValue.java +++ b/src/main/java/com/cleanroommc/modularui/value/sync/FloatSyncValue.java @@ -72,6 +72,7 @@ public float getFloatValue() { @Override public void setFloatValue(float value, boolean setSource, boolean sync) { this.cache = value; + onValueChanged(); if (setSource && this.setter != null) { this.setter.accept(value); } diff --git a/src/main/java/com/cleanroommc/modularui/value/sync/GenericCollectionSyncHandler.java b/src/main/java/com/cleanroommc/modularui/value/sync/GenericCollectionSyncHandler.java index be0f9d456..9bc9f32ae 100644 --- a/src/main/java/com/cleanroommc/modularui/value/sync/GenericCollectionSyncHandler.java +++ b/src/main/java/com/cleanroommc/modularui/value/sync/GenericCollectionSyncHandler.java @@ -49,6 +49,7 @@ public void setValue(C value, boolean setSource, boolean sync) { protected abstract void setCache(C value); protected void onSetCache(C value, boolean setSource, boolean sync) { + onValueChanged(); if (setSource && this.setter != null) { this.setter.accept(value); } diff --git a/src/main/java/com/cleanroommc/modularui/value/sync/GenericMapSyncHandler.java b/src/main/java/com/cleanroommc/modularui/value/sync/GenericMapSyncHandler.java index 4e864e0b8..9731712a3 100644 --- a/src/main/java/com/cleanroommc/modularui/value/sync/GenericMapSyncHandler.java +++ b/src/main/java/com/cleanroommc/modularui/value/sync/GenericMapSyncHandler.java @@ -56,6 +56,7 @@ public void setValue(Map value, boolean setSource, boolean sync) { for (Map.Entry entry : value.entrySet()) { this.cache.put(this.keyCopy.createDeepCopy(entry.getKey()), this.valueCopy.createDeepCopy(entry.getValue())); } + onValueChanged(); if (setSource && this.setter != null) { this.setter.accept(value); } diff --git a/src/main/java/com/cleanroommc/modularui/value/sync/GenericSyncValue.java b/src/main/java/com/cleanroommc/modularui/value/sync/GenericSyncValue.java index 2d8b9664b..0c9575b23 100644 --- a/src/main/java/com/cleanroommc/modularui/value/sync/GenericSyncValue.java +++ b/src/main/java/com/cleanroommc/modularui/value/sync/GenericSyncValue.java @@ -112,6 +112,7 @@ public T getValue() { @Override public void setValue(T value, boolean setSource, boolean sync) { this.cache = this.copy.createDeepCopy(value); + onValueChanged(); if (setSource && this.setter != null) { this.setter.accept(value); } diff --git a/src/main/java/com/cleanroommc/modularui/value/sync/IntSyncValue.java b/src/main/java/com/cleanroommc/modularui/value/sync/IntSyncValue.java index 100cdf2d8..e420ed3e0 100644 --- a/src/main/java/com/cleanroommc/modularui/value/sync/IntSyncValue.java +++ b/src/main/java/com/cleanroommc/modularui/value/sync/IntSyncValue.java @@ -71,6 +71,7 @@ public void setValue(Integer value, boolean setSource, boolean sync) { @Override public void setIntValue(int value, boolean setSource, boolean sync) { this.cache = value; + onValueChanged(); if (setSource && this.setter != null) { this.setter.accept(value); } diff --git a/src/main/java/com/cleanroommc/modularui/value/sync/LongSyncValue.java b/src/main/java/com/cleanroommc/modularui/value/sync/LongSyncValue.java index 25e1f02a1..d375aafec 100644 --- a/src/main/java/com/cleanroommc/modularui/value/sync/LongSyncValue.java +++ b/src/main/java/com/cleanroommc/modularui/value/sync/LongSyncValue.java @@ -71,6 +71,7 @@ public void setValue(Long value, boolean setSource, boolean sync) { @Override public void setLongValue(long value, boolean setSource, boolean sync) { this.cache = value; + onValueChanged(); if (setSource && this.setter != null) { this.setter.accept(value); } diff --git a/src/main/java/com/cleanroommc/modularui/value/sync/StringSyncValue.java b/src/main/java/com/cleanroommc/modularui/value/sync/StringSyncValue.java index 4cdb987df..2f217c568 100644 --- a/src/main/java/com/cleanroommc/modularui/value/sync/StringSyncValue.java +++ b/src/main/java/com/cleanroommc/modularui/value/sync/StringSyncValue.java @@ -69,6 +69,7 @@ public void setValue(String value, boolean setSource, boolean sync) { @Override public void setStringValue(String value, boolean setSource, boolean sync) { this.cache = value; + onValueChanged(); if (setSource && this.setter != null) { this.setter.accept(value); } diff --git a/src/main/java/com/cleanroommc/modularui/value/sync/ValueSyncHandler.java b/src/main/java/com/cleanroommc/modularui/value/sync/ValueSyncHandler.java index 391ad6d5f..49476ff2d 100644 --- a/src/main/java/com/cleanroommc/modularui/value/sync/ValueSyncHandler.java +++ b/src/main/java/com/cleanroommc/modularui/value/sync/ValueSyncHandler.java @@ -13,13 +13,11 @@ public abstract class ValueSyncHandler extends SyncHandler implements IValueS @Override public void readOnClient(int id, PacketBuffer buf) throws IOException { read(buf); - onValueChanged(); } @Override public void readOnServer(int id, PacketBuffer buf) throws IOException { read(buf); - onValueChanged(); } @Override @@ -29,6 +27,10 @@ public void detectAndSendChanges(boolean init) { } } + /** + * Called when the cached value of this sync handler updates. Implementations need to call this inside + * {@link #setValue(Object, boolean, boolean)}. + */ protected void onValueChanged() { if (this.changeListener != null) { this.changeListener.run(); From 5da978f6e2224c9d08cf07db6318221dc07c59eb Mon Sep 17 00:00:00 2001 From: brachy84 Date: Sat, 29 Nov 2025 16:01:16 +0100 Subject: [PATCH 03/50] minor sync value improvement (cherry picked from commit a2fe5691c3469cacac5c88176bb581f017de7ff2) --- .../modularui/value/sync/BooleanSyncValue.java | 4 +--- .../modularui/value/sync/ByteSyncValue.java | 4 +--- .../modularui/value/sync/DoubleSyncValue.java | 4 +--- .../modularui/value/sync/EnumSyncValue.java | 4 +--- .../modularui/value/sync/FloatSyncValue.java | 5 +---- .../modularui/value/sync/FluidSlotSyncHandler.java | 13 +++---------- .../value/sync/GenericCollectionSyncHandler.java | 4 +--- .../value/sync/GenericMapSyncHandler.java | 4 +--- .../modularui/value/sync/GenericSyncValue.java | 6 +++--- .../modularui/value/sync/IntSyncValue.java | 4 +--- .../modularui/value/sync/ItemSlotSH.java | 5 ++--- .../modularui/value/sync/LongSyncValue.java | 4 +--- .../modularui/value/sync/StringSyncValue.java | 4 +--- .../modularui/value/sync/ValueSyncHandler.java | 14 +++++++++----- 14 files changed, 27 insertions(+), 52 deletions(-) diff --git a/src/main/java/com/cleanroommc/modularui/value/sync/BooleanSyncValue.java b/src/main/java/com/cleanroommc/modularui/value/sync/BooleanSyncValue.java index d17898342..f2cf7a3ea 100644 --- a/src/main/java/com/cleanroommc/modularui/value/sync/BooleanSyncValue.java +++ b/src/main/java/com/cleanroommc/modularui/value/sync/BooleanSyncValue.java @@ -74,9 +74,7 @@ public void setBoolValue(boolean value, boolean setSource, boolean sync) { if (setSource && this.setter != null) { this.setter.accept(value); } - if (sync) { - sync(0, this::write); - } + if (sync) sync(); } @Override diff --git a/src/main/java/com/cleanroommc/modularui/value/sync/ByteSyncValue.java b/src/main/java/com/cleanroommc/modularui/value/sync/ByteSyncValue.java index f01067706..a5aae8052 100644 --- a/src/main/java/com/cleanroommc/modularui/value/sync/ByteSyncValue.java +++ b/src/main/java/com/cleanroommc/modularui/value/sync/ByteSyncValue.java @@ -90,9 +90,7 @@ public void setByteValue(byte value, boolean setSource, boolean sync) { if (setSource && this.setter != null) { this.setter.setByte(value); } - if (sync) { - sync(0, this::write); - } + if (sync) sync(); } @Override diff --git a/src/main/java/com/cleanroommc/modularui/value/sync/DoubleSyncValue.java b/src/main/java/com/cleanroommc/modularui/value/sync/DoubleSyncValue.java index 61a7fa38d..0d5bd8797 100644 --- a/src/main/java/com/cleanroommc/modularui/value/sync/DoubleSyncValue.java +++ b/src/main/java/com/cleanroommc/modularui/value/sync/DoubleSyncValue.java @@ -75,9 +75,7 @@ public void setDoubleValue(double value, boolean setSource, boolean sync) { if (setSource && this.setter != null) { this.setter.accept(value); } - if (sync) { - sync(0, this::write); - } + if (sync) sync(); } @Override diff --git a/src/main/java/com/cleanroommc/modularui/value/sync/EnumSyncValue.java b/src/main/java/com/cleanroommc/modularui/value/sync/EnumSyncValue.java index c47bcae4d..a7e5f57b2 100644 --- a/src/main/java/com/cleanroommc/modularui/value/sync/EnumSyncValue.java +++ b/src/main/java/com/cleanroommc/modularui/value/sync/EnumSyncValue.java @@ -73,9 +73,7 @@ public void setValue(T value, boolean setSource, boolean sync) { if (setSource && this.setter != null) { this.setter.accept(value); } - if (sync) { - sync(0, this::write); - } + if (sync) sync(); } @Override diff --git a/src/main/java/com/cleanroommc/modularui/value/sync/FloatSyncValue.java b/src/main/java/com/cleanroommc/modularui/value/sync/FloatSyncValue.java index 8dd09ef36..8871a6fde 100644 --- a/src/main/java/com/cleanroommc/modularui/value/sync/FloatSyncValue.java +++ b/src/main/java/com/cleanroommc/modularui/value/sync/FloatSyncValue.java @@ -1,6 +1,5 @@ package com.cleanroommc.modularui.value.sync; -import com.cleanroommc.modularui.api.value.IDoubleValue; import com.cleanroommc.modularui.api.value.sync.IDoubleSyncValue; import com.cleanroommc.modularui.api.value.sync.IFloatSyncValue; import com.cleanroommc.modularui.api.value.sync.IStringSyncValue; @@ -76,9 +75,7 @@ public void setFloatValue(float value, boolean setSource, boolean sync) { if (setSource && this.setter != null) { this.setter.accept(value); } - if (sync) { - sync(0, this::write); - } + if (sync) sync(); } @Override diff --git a/src/main/java/com/cleanroommc/modularui/value/sync/FluidSlotSyncHandler.java b/src/main/java/com/cleanroommc/modularui/value/sync/FluidSlotSyncHandler.java index 1a254705a..d3b94bbb4 100644 --- a/src/main/java/com/cleanroommc/modularui/value/sync/FluidSlotSyncHandler.java +++ b/src/main/java/com/cleanroommc/modularui/value/sync/FluidSlotSyncHandler.java @@ -25,7 +25,6 @@ public static FluidStack copyFluid(@Nullable FluidStack fluidStack) { return isFluidEmpty(fluidStack) ? null : fluidStack.copy(); } - public static final int SYNC_FLUID = 0; public static final int SYNC_CLICK = 1; public static final int SYNC_SCROLL = 2; public static final int SYNC_CONTROLS_AMOUNT = 3; @@ -56,13 +55,7 @@ public void setValue(@Nullable FluidStack value, boolean setSource, boolean sync this.fluidTank.fill(value.copy(), true); } } - if (sync) { - if (NetworkUtils.isClient()) { - syncToServer(SYNC_FLUID, this::write); - } else { - syncToClient(SYNC_FLUID, this::write); - } - } + if (sync) sync(); onValueChanged(); } @@ -101,7 +94,7 @@ public void read(PacketBuffer buffer) { @Override public void readOnClient(int id, PacketBuffer buf) { - if (id == SYNC_FLUID) { + if (id == SYNC_VALUE) { read(buf); } else if (id == SYNC_CONTROLS_AMOUNT) { this.controlsAmount = buf.readBoolean(); @@ -110,7 +103,7 @@ public void readOnClient(int id, PacketBuffer buf) { @Override public void readOnServer(int id, PacketBuffer buf) { - if (id == SYNC_FLUID) { + if (id == SYNC_VALUE) { if (this.phantom) { read(buf); } diff --git a/src/main/java/com/cleanroommc/modularui/value/sync/GenericCollectionSyncHandler.java b/src/main/java/com/cleanroommc/modularui/value/sync/GenericCollectionSyncHandler.java index 9bc9f32ae..0a8e600ef 100644 --- a/src/main/java/com/cleanroommc/modularui/value/sync/GenericCollectionSyncHandler.java +++ b/src/main/java/com/cleanroommc/modularui/value/sync/GenericCollectionSyncHandler.java @@ -53,9 +53,7 @@ protected void onSetCache(C value, boolean setSource, boolean sync) { if (setSource && this.setter != null) { this.setter.accept(value); } - if (sync) { - sync(0, this::write); - } + if (sync) sync(); } @Override diff --git a/src/main/java/com/cleanroommc/modularui/value/sync/GenericMapSyncHandler.java b/src/main/java/com/cleanroommc/modularui/value/sync/GenericMapSyncHandler.java index 9731712a3..369b34b3b 100644 --- a/src/main/java/com/cleanroommc/modularui/value/sync/GenericMapSyncHandler.java +++ b/src/main/java/com/cleanroommc/modularui/value/sync/GenericMapSyncHandler.java @@ -60,9 +60,7 @@ public void setValue(Map value, boolean setSource, boolean sync) { if (setSource && this.setter != null) { this.setter.accept(value); } - if (sync) { - sync(0, this::write); - } + if (sync) sync(); } @Override diff --git a/src/main/java/com/cleanroommc/modularui/value/sync/GenericSyncValue.java b/src/main/java/com/cleanroommc/modularui/value/sync/GenericSyncValue.java index 0c9575b23..8b75bc3da 100644 --- a/src/main/java/com/cleanroommc/modularui/value/sync/GenericSyncValue.java +++ b/src/main/java/com/cleanroommc/modularui/value/sync/GenericSyncValue.java @@ -116,13 +116,12 @@ public void setValue(T value, boolean setSource, boolean sync) { if (setSource && this.setter != null) { this.setter.accept(value); } - if (sync) { - sync(0, this::write); - } + if (sync) sync(); } @Override public boolean updateCacheFromSource(boolean isFirstSync) { + if (this.getter == null) return false; T t = this.getter.get(); if (isFirstSync || !this.equals.areEqual(this.cache, t)) { setValue(t, false, false); @@ -133,6 +132,7 @@ public boolean updateCacheFromSource(boolean isFirstSync) { @Override public void notifyUpdate() { + if (this.getter == null) throw new NullPointerException("Can't notify sync handler with null getter."); setValue(this.getter.get(), false, true); } diff --git a/src/main/java/com/cleanroommc/modularui/value/sync/IntSyncValue.java b/src/main/java/com/cleanroommc/modularui/value/sync/IntSyncValue.java index e420ed3e0..549856024 100644 --- a/src/main/java/com/cleanroommc/modularui/value/sync/IntSyncValue.java +++ b/src/main/java/com/cleanroommc/modularui/value/sync/IntSyncValue.java @@ -75,9 +75,7 @@ public void setIntValue(int value, boolean setSource, boolean sync) { if (setSource && this.setter != null) { this.setter.accept(value); } - if (sync) { - sync(0, this::write); - } + if (sync) sync(); } @Override diff --git a/src/main/java/com/cleanroommc/modularui/value/sync/ItemSlotSH.java b/src/main/java/com/cleanroommc/modularui/value/sync/ItemSlotSH.java index 5060c56f1..84ee7f613 100644 --- a/src/main/java/com/cleanroommc/modularui/value/sync/ItemSlotSH.java +++ b/src/main/java/com/cleanroommc/modularui/value/sync/ItemSlotSH.java @@ -3,7 +3,6 @@ import com.cleanroommc.modularui.network.NetworkUtils; import com.cleanroommc.modularui.utils.item.ItemHandlerHelper; import com.cleanroommc.modularui.widgets.slot.ModularSlot; - import com.cleanroommc.modularui.widgets.slot.PlayerSlotType; import net.minecraft.item.ItemStack; @@ -19,8 +18,8 @@ */ public class ItemSlotSH extends SyncHandler { - public static final int SYNC_ITEM = 1; - public static final int SYNC_ENABLED = 2; + public static final int SYNC_ITEM = 0; + public static final int SYNC_ENABLED = 1; private final ModularSlot slot; private final PlayerSlotType playerSlotType; diff --git a/src/main/java/com/cleanroommc/modularui/value/sync/LongSyncValue.java b/src/main/java/com/cleanroommc/modularui/value/sync/LongSyncValue.java index d375aafec..6c2e00c09 100644 --- a/src/main/java/com/cleanroommc/modularui/value/sync/LongSyncValue.java +++ b/src/main/java/com/cleanroommc/modularui/value/sync/LongSyncValue.java @@ -75,9 +75,7 @@ public void setLongValue(long value, boolean setSource, boolean sync) { if (setSource && this.setter != null) { this.setter.accept(value); } - if (sync) { - sync(0, this::write); - } + if (sync) sync(); } @Override diff --git a/src/main/java/com/cleanroommc/modularui/value/sync/StringSyncValue.java b/src/main/java/com/cleanroommc/modularui/value/sync/StringSyncValue.java index 2f217c568..9f357ad05 100644 --- a/src/main/java/com/cleanroommc/modularui/value/sync/StringSyncValue.java +++ b/src/main/java/com/cleanroommc/modularui/value/sync/StringSyncValue.java @@ -73,9 +73,7 @@ public void setStringValue(String value, boolean setSource, boolean sync) { if (setSource && this.setter != null) { this.setter.accept(value); } - if (sync) { - sync(0, this::write); - } + if (sync) sync(); } @Override diff --git a/src/main/java/com/cleanroommc/modularui/value/sync/ValueSyncHandler.java b/src/main/java/com/cleanroommc/modularui/value/sync/ValueSyncHandler.java index 49476ff2d..b71c9522e 100644 --- a/src/main/java/com/cleanroommc/modularui/value/sync/ValueSyncHandler.java +++ b/src/main/java/com/cleanroommc/modularui/value/sync/ValueSyncHandler.java @@ -8,23 +8,27 @@ public abstract class ValueSyncHandler extends SyncHandler implements IValueSyncHandler { + public static final int SYNC_VALUE = 0; + private Runnable changeListener; @Override public void readOnClient(int id, PacketBuffer buf) throws IOException { - read(buf); + if (id == SYNC_VALUE) read(buf); } @Override public void readOnServer(int id, PacketBuffer buf) throws IOException { - read(buf); + if (id == SYNC_VALUE) read(buf); + } + + protected void sync() { + sync(SYNC_VALUE, this::write); } @Override public void detectAndSendChanges(boolean init) { - if (updateCacheFromSource(init)) { - syncToClient(0, this::write); - } + if (updateCacheFromSource(init)) sync(); } /** From dabc6529b91f736345a3367f7f7c78a2b9baebb9 Mon Sep 17 00:00:00 2001 From: brachy84 Date: Sat, 29 Nov 2025 16:03:39 +0100 Subject: [PATCH 04/50] call change listener after set source (cherry picked from commit 2d05eea1678d8d73d8f9cf27ab2ec38133e25a38) --- .../com/cleanroommc/modularui/value/sync/BooleanSyncValue.java | 2 +- .../com/cleanroommc/modularui/value/sync/ByteSyncValue.java | 2 +- .../com/cleanroommc/modularui/value/sync/DoubleSyncValue.java | 2 +- .../com/cleanroommc/modularui/value/sync/EnumSyncValue.java | 2 +- .../com/cleanroommc/modularui/value/sync/FloatSyncValue.java | 2 +- .../cleanroommc/modularui/value/sync/FluidSlotSyncHandler.java | 2 +- .../modularui/value/sync/GenericCollectionSyncHandler.java | 2 +- .../cleanroommc/modularui/value/sync/GenericMapSyncHandler.java | 2 +- .../com/cleanroommc/modularui/value/sync/GenericSyncValue.java | 2 +- .../java/com/cleanroommc/modularui/value/sync/IntSyncValue.java | 2 +- .../com/cleanroommc/modularui/value/sync/LongSyncValue.java | 2 +- .../com/cleanroommc/modularui/value/sync/StringSyncValue.java | 2 +- 12 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/main/java/com/cleanroommc/modularui/value/sync/BooleanSyncValue.java b/src/main/java/com/cleanroommc/modularui/value/sync/BooleanSyncValue.java index f2cf7a3ea..4f5020f47 100644 --- a/src/main/java/com/cleanroommc/modularui/value/sync/BooleanSyncValue.java +++ b/src/main/java/com/cleanroommc/modularui/value/sync/BooleanSyncValue.java @@ -70,10 +70,10 @@ public void setValue(@NotNull Boolean value, boolean setSource, boolean sync) { @Override public void setBoolValue(boolean value, boolean setSource, boolean sync) { this.cache = value; - onValueChanged(); if (setSource && this.setter != null) { this.setter.accept(value); } + onValueChanged(); if (sync) sync(); } diff --git a/src/main/java/com/cleanroommc/modularui/value/sync/ByteSyncValue.java b/src/main/java/com/cleanroommc/modularui/value/sync/ByteSyncValue.java index a5aae8052..c7385a0b0 100644 --- a/src/main/java/com/cleanroommc/modularui/value/sync/ByteSyncValue.java +++ b/src/main/java/com/cleanroommc/modularui/value/sync/ByteSyncValue.java @@ -86,10 +86,10 @@ public Byte getValue() { @Override public void setByteValue(byte value, boolean setSource, boolean sync) { this.cache = value; - onValueChanged(); if (setSource && this.setter != null) { this.setter.setByte(value); } + onValueChanged(); if (sync) sync(); } diff --git a/src/main/java/com/cleanroommc/modularui/value/sync/DoubleSyncValue.java b/src/main/java/com/cleanroommc/modularui/value/sync/DoubleSyncValue.java index 0d5bd8797..ca78ff139 100644 --- a/src/main/java/com/cleanroommc/modularui/value/sync/DoubleSyncValue.java +++ b/src/main/java/com/cleanroommc/modularui/value/sync/DoubleSyncValue.java @@ -71,10 +71,10 @@ public void setValue(@NotNull Double value, boolean setSource, boolean sync) { @Override public void setDoubleValue(double value, boolean setSource, boolean sync) { this.cache = value; - onValueChanged(); if (setSource && this.setter != null) { this.setter.accept(value); } + onValueChanged(); if (sync) sync(); } diff --git a/src/main/java/com/cleanroommc/modularui/value/sync/EnumSyncValue.java b/src/main/java/com/cleanroommc/modularui/value/sync/EnumSyncValue.java index a7e5f57b2..baf6b2d8c 100644 --- a/src/main/java/com/cleanroommc/modularui/value/sync/EnumSyncValue.java +++ b/src/main/java/com/cleanroommc/modularui/value/sync/EnumSyncValue.java @@ -69,10 +69,10 @@ public T getValue() { @Override public void setValue(T value, boolean setSource, boolean sync) { this.cache = value; - onValueChanged(); if (setSource && this.setter != null) { this.setter.accept(value); } + onValueChanged(); if (sync) sync(); } diff --git a/src/main/java/com/cleanroommc/modularui/value/sync/FloatSyncValue.java b/src/main/java/com/cleanroommc/modularui/value/sync/FloatSyncValue.java index 8871a6fde..9501bd029 100644 --- a/src/main/java/com/cleanroommc/modularui/value/sync/FloatSyncValue.java +++ b/src/main/java/com/cleanroommc/modularui/value/sync/FloatSyncValue.java @@ -71,10 +71,10 @@ public float getFloatValue() { @Override public void setFloatValue(float value, boolean setSource, boolean sync) { this.cache = value; - onValueChanged(); if (setSource && this.setter != null) { this.setter.accept(value); } + onValueChanged(); if (sync) sync(); } diff --git a/src/main/java/com/cleanroommc/modularui/value/sync/FluidSlotSyncHandler.java b/src/main/java/com/cleanroommc/modularui/value/sync/FluidSlotSyncHandler.java index d3b94bbb4..12a4f36db 100644 --- a/src/main/java/com/cleanroommc/modularui/value/sync/FluidSlotSyncHandler.java +++ b/src/main/java/com/cleanroommc/modularui/value/sync/FluidSlotSyncHandler.java @@ -55,8 +55,8 @@ public void setValue(@Nullable FluidStack value, boolean setSource, boolean sync this.fluidTank.fill(value.copy(), true); } } - if (sync) sync(); onValueChanged(); + if (sync) sync(); } public boolean needsSync() { diff --git a/src/main/java/com/cleanroommc/modularui/value/sync/GenericCollectionSyncHandler.java b/src/main/java/com/cleanroommc/modularui/value/sync/GenericCollectionSyncHandler.java index 0a8e600ef..8d97948dc 100644 --- a/src/main/java/com/cleanroommc/modularui/value/sync/GenericCollectionSyncHandler.java +++ b/src/main/java/com/cleanroommc/modularui/value/sync/GenericCollectionSyncHandler.java @@ -49,10 +49,10 @@ public void setValue(C value, boolean setSource, boolean sync) { protected abstract void setCache(C value); protected void onSetCache(C value, boolean setSource, boolean sync) { - onValueChanged(); if (setSource && this.setter != null) { this.setter.accept(value); } + onValueChanged(); if (sync) sync(); } diff --git a/src/main/java/com/cleanroommc/modularui/value/sync/GenericMapSyncHandler.java b/src/main/java/com/cleanroommc/modularui/value/sync/GenericMapSyncHandler.java index 369b34b3b..6dca9f934 100644 --- a/src/main/java/com/cleanroommc/modularui/value/sync/GenericMapSyncHandler.java +++ b/src/main/java/com/cleanroommc/modularui/value/sync/GenericMapSyncHandler.java @@ -56,10 +56,10 @@ public void setValue(Map value, boolean setSource, boolean sync) { for (Map.Entry entry : value.entrySet()) { this.cache.put(this.keyCopy.createDeepCopy(entry.getKey()), this.valueCopy.createDeepCopy(entry.getValue())); } - onValueChanged(); if (setSource && this.setter != null) { this.setter.accept(value); } + onValueChanged(); if (sync) sync(); } diff --git a/src/main/java/com/cleanroommc/modularui/value/sync/GenericSyncValue.java b/src/main/java/com/cleanroommc/modularui/value/sync/GenericSyncValue.java index 8b75bc3da..dbf9711a6 100644 --- a/src/main/java/com/cleanroommc/modularui/value/sync/GenericSyncValue.java +++ b/src/main/java/com/cleanroommc/modularui/value/sync/GenericSyncValue.java @@ -112,10 +112,10 @@ public T getValue() { @Override public void setValue(T value, boolean setSource, boolean sync) { this.cache = this.copy.createDeepCopy(value); - onValueChanged(); if (setSource && this.setter != null) { this.setter.accept(value); } + onValueChanged(); if (sync) sync(); } diff --git a/src/main/java/com/cleanroommc/modularui/value/sync/IntSyncValue.java b/src/main/java/com/cleanroommc/modularui/value/sync/IntSyncValue.java index 549856024..f3144ddc7 100644 --- a/src/main/java/com/cleanroommc/modularui/value/sync/IntSyncValue.java +++ b/src/main/java/com/cleanroommc/modularui/value/sync/IntSyncValue.java @@ -71,10 +71,10 @@ public void setValue(Integer value, boolean setSource, boolean sync) { @Override public void setIntValue(int value, boolean setSource, boolean sync) { this.cache = value; - onValueChanged(); if (setSource && this.setter != null) { this.setter.accept(value); } + onValueChanged(); if (sync) sync(); } diff --git a/src/main/java/com/cleanroommc/modularui/value/sync/LongSyncValue.java b/src/main/java/com/cleanroommc/modularui/value/sync/LongSyncValue.java index 6c2e00c09..80195ac68 100644 --- a/src/main/java/com/cleanroommc/modularui/value/sync/LongSyncValue.java +++ b/src/main/java/com/cleanroommc/modularui/value/sync/LongSyncValue.java @@ -71,10 +71,10 @@ public void setValue(Long value, boolean setSource, boolean sync) { @Override public void setLongValue(long value, boolean setSource, boolean sync) { this.cache = value; - onValueChanged(); if (setSource && this.setter != null) { this.setter.accept(value); } + onValueChanged(); if (sync) sync(); } diff --git a/src/main/java/com/cleanroommc/modularui/value/sync/StringSyncValue.java b/src/main/java/com/cleanroommc/modularui/value/sync/StringSyncValue.java index 9f357ad05..3da601895 100644 --- a/src/main/java/com/cleanroommc/modularui/value/sync/StringSyncValue.java +++ b/src/main/java/com/cleanroommc/modularui/value/sync/StringSyncValue.java @@ -69,10 +69,10 @@ public void setValue(String value, boolean setSource, boolean sync) { @Override public void setStringValue(String value, boolean setSource, boolean sync) { this.cache = value; - onValueChanged(); if (setSource && this.setter != null) { this.setter.accept(value); } + onValueChanged(); if (sync) sync(); } From 2e2a9686f71544aebcbea078c0fb409985131c30 Mon Sep 17 00:00:00 2001 From: brachy84 Date: Sat, 29 Nov 2025 18:17:15 +0100 Subject: [PATCH 05/50] more readable panel sync handler syncing (cherry picked from commit 57b58b39cf4dd8e4154ad13549c7fd25fe51cd0b) --- .../value/sync/PanelSyncHandler.java | 32 +++++++++++-------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/src/main/java/com/cleanroommc/modularui/value/sync/PanelSyncHandler.java b/src/main/java/com/cleanroommc/modularui/value/sync/PanelSyncHandler.java index 246e59950..58802bd94 100644 --- a/src/main/java/com/cleanroommc/modularui/value/sync/PanelSyncHandler.java +++ b/src/main/java/com/cleanroommc/modularui/value/sync/PanelSyncHandler.java @@ -21,6 +21,11 @@ */ public final class PanelSyncHandler extends SyncHandler implements IPanelHandler { + public static final int SYNC_NOTIFY_OPEN = 0; + public static final int SYNC_OPEN = 1; + public static final int SYNC_CLOSE = 2; + public static final int SYNC_DISPOSE = 3; + private final IPanelBuilder panelBuilder; private final boolean subPanel; private String panelName; @@ -51,7 +56,7 @@ private void openPanel(boolean syncToServer) { if (isPanelOpen()) return; boolean client = getSyncManager().isClient(); if (syncToServer && client) { - syncToServer(0); + syncToServer(SYNC_NOTIFY_OPEN); return; } if (this.syncManager != null && this.syncManager.getModularSyncManager() != getSyncManager().getModularSyncManager()) { @@ -94,7 +99,7 @@ public void closePanel() { this.openedPanel.closeIfOpen(); } } else { - syncToClient(2); + syncToClient(SYNC_CLOSE); } } @@ -109,7 +114,7 @@ public void closePanelInternal() { getSyncManager().getModularSyncManager().close(this.panelName); this.open = false; if (getSyncManager().isClient()) { - syncToServer(2); + syncToServer(SYNC_CLOSE); } } @@ -117,7 +122,6 @@ public void closePanelInternal() { public void deleteCachedPanel() { if (openedPanel == null || isPanelOpen()) return; boolean canDispose = WidgetTree.foreachChild(openedPanel, iWidget -> { - if (!iWidget.isValid()) return false; if (iWidget instanceof ISynced synced && synced.isSynced()) { return !(synced.getSyncHandler() instanceof ItemSlotSH); } @@ -126,12 +130,12 @@ public void deleteCachedPanel() { // This is because we can't guarantee that the sync handlers of the new panel are the same. // Dynamic sync handler changing is very error-prone. - if (!canDispose) + if (!canDispose) { throw new UnsupportedOperationException("Can't delete cached panel if it's still open or has ItemSlot Sync Handlers!"); + } disposePanel(); - - sync(3); + sync(SYNC_DISPOSE); } private void disposePanel() { @@ -152,23 +156,23 @@ public boolean isPanelOpen() { @Override public void readOnClient(int i, PacketBuffer packetBuffer) throws IOException { - if (i == 1) { + if (i == SYNC_OPEN) { openPanel(false); - } else if (i == 2) { + } else if (i == SYNC_CLOSE) { closePanel(); - } else if (i == 3) { + } else if (i == SYNC_DISPOSE) { disposePanel(); } } @Override public void readOnServer(int i, PacketBuffer packetBuffer) throws IOException { - if (i == 0) { + if (i == SYNC_NOTIFY_OPEN) { openPanel(false); - syncToClient(1); - } else if (i == 2) { + syncToClient(SYNC_OPEN); + } else if (i == SYNC_CLOSE) { closePanelInternal(); - } else if (i == 3) { + } else if (i == SYNC_DISPOSE) { disposePanel(); } } From 33c004ac086922d2c8609ae8ad66e2fdbef2a413 Mon Sep 17 00:00:00 2001 From: brachy84 Date: Sat, 29 Nov 2025 21:20:28 +0100 Subject: [PATCH 06/50] disallow registering panel sync handlers late (cherry picked from commit 6f4e395fea73c576b31e06efea6e87c60c9283cb) --- .../value/sync/PanelSyncManager.java | 71 ++++++++++++++----- 1 file changed, 55 insertions(+), 16 deletions(-) diff --git a/src/main/java/com/cleanroommc/modularui/value/sync/PanelSyncManager.java b/src/main/java/com/cleanroommc/modularui/value/sync/PanelSyncManager.java index 3c687dc37..636645767 100644 --- a/src/main/java/com/cleanroommc/modularui/value/sync/PanelSyncManager.java +++ b/src/main/java/com/cleanroommc/modularui/value/sync/PanelSyncManager.java @@ -40,7 +40,7 @@ public class PanelSyncManager { private final Map slotGroups = new Object2ObjectOpenHashMap<>(); private final Map reverseSyncHandlers = new Object2ObjectOpenHashMap<>(); private final Map syncedActions = new Object2ObjectOpenHashMap<>(); - private final Map subPanels = new Object2ObjectArrayMap<>(); + private final Map subPanels = new Object2ObjectArrayMap<>(); private ModularSyncManager modularSyncManager; private String panelName; private boolean init = true; @@ -69,9 +69,15 @@ public void initialize(String panelName, ModularSyncManager msm) { private void registerPanelSyncHandler(String name, SyncHandler syncHandler) { // only called on main psm SyncHandler currentSh = this.syncHandlers.get(name); - if (currentSh != null && currentSh != syncHandler) throw new IllegalStateException(); + if (currentSh != null && currentSh != syncHandler) { + throw new IllegalStateException("Failed to register panel sync handler during initialization. " + + "There already exists a sync handler for the name '" + name + "'."); + } String currentName = this.reverseSyncHandlers.get(syncHandler); - if (currentName != null && !name.equals(currentName)) throw new IllegalStateException(); + if (currentName != null && !name.equals(currentName)) { + throw new IllegalStateException("Failed to register panel sync handler for name '" + name + "' during initialization. " + + "The panel sync handler is already registered under the name '" + currentName + "'."); + } this.syncHandlers.put(name, syncHandler); this.reverseSyncHandlers.put(syncHandler, name); syncHandler.init(name, this); @@ -79,12 +85,8 @@ private void registerPanelSyncHandler(String name, SyncHandler syncHandler) { void closeSubPanels() { this.subPanels.values().forEach(syncHandler -> { - if (syncHandler instanceof IPanelHandler panelHandler) { - if (panelHandler.isSubPanel()) { - panelHandler.closePanel(); - } - } else { - throw new IllegalStateException(); + if (syncHandler.isSubPanel()) { + syncHandler.closePanel(); } }); } @@ -142,7 +144,7 @@ private boolean invokeSyncedAction(String mapKey, PacketBuffer buf) { ModularUI.LOGGER.warn("SyncAction '{}' does not exist for panel '{}'!.", mapKey, panelName); return false; } - if (this.allowSyncHandlerRegistration || !syncedAction.isExecuteClient() || !syncedAction.isExecuteServer()) { + if (!isLocked() || this.allowSyncHandlerRegistration || !syncedAction.isExecuteClient() || !syncedAction.isExecuteServer()) { syncedAction.invoke(this.client, buf); } else { // only allow sync handler registration if it is executed on client and server @@ -234,28 +236,65 @@ public DynamicSyncHandler dynamicSyncHandler(String key, int id, DynamicSyncHand return syncHandler; } + /** + * @deprecated replaced by {@link #syncedPanel(String, boolean, PanelSyncHandler.IPanelBuilder)} + */ + @ApiStatus.ScheduledForRemoval(inVersion = "3.3.0") + @Deprecated + public IPanelHandler panel(String key, PanelSyncHandler.IPanelBuilder panelBuilder, boolean subPanel) { + SyncHandler sh = this.subPanels.get(key); + if (sh != null) return (IPanelHandler) sh; + PanelSyncHandler syncHandler = new PanelSyncHandler(panelBuilder, subPanel); + this.subPanels.put(key, syncHandler); + return syncHandler; + } + /** * Creates a synced panel handler. This can be used to automatically handle syncing for synced panels. * Synced panels do not need to be synced themselves, but contain at least one widget which is synced. - *

NOTE

+ *

NOTE

* A panel sync handler is only created once. If one was already registered, that one will be returned. - * (This is only relevant for nested sub panels.) + * (This is only relevant for nested sub panels.) Furthermore, the panel handler has to be created on client and server with the same + * key. Like any other sync handler, the panel sync handler has to be created before the panel opened. The only exception is inside + * dynamic sync handlers. * * @param key the key used for syncing - * @param panelBuilder the panel builder, that will create the new panel. It must not return null or any existing panels. * @param subPanel true if this panel should close when its parent closes (the parent is defined by this {@link PanelSyncManager}) + * @param panelBuilder the panel builder, that will create the new panel. It must not return null or any existing panels. * @return a synced panel handler. * @throws NullPointerException if the build panel of the builder is null * @throws IllegalArgumentException if the build panel of the builder is the main panel + * @throws IllegalStateException if this method was called too late */ - public IPanelHandler panel(String key, PanelSyncHandler.IPanelBuilder panelBuilder, boolean subPanel) { - SyncHandler sh = this.subPanels.get(key); - if (sh != null) return (IPanelHandler) sh; + public IPanelHandler syncedPanel(String key, boolean subPanel, PanelSyncHandler.IPanelBuilder panelBuilder) { + IPanelHandler ph = findPanelHandlerNullable(key); + if (ph != null) return ph; + if (isLocked() && !this.allowSyncHandlerRegistration) { + // registration of sync handlers forbidden + throw new IllegalStateException("Synced panels must be registered during panel building. The only exceptions is via a DynamicSyncHandler and sync functions!"); + } PanelSyncHandler syncHandler = new PanelSyncHandler(panelBuilder, subPanel); this.subPanels.put(key, syncHandler); + if (isInitialised() && (this == this.modularSyncManager.getMainPSM() || + this.modularSyncManager.getMainPSM().findSyncHandlerNullable(this.panelName, PanelSyncHandler.class) == null)) { + // current panel is open + this.modularSyncManager.getMainPSM().registerPanelSyncHandler(key, syncHandler); + } return syncHandler; } + public @Nullable IPanelHandler findPanelHandlerNullable(String key) { + return this.subPanels.get(key); + } + + public @NotNull IPanelHandler findPanelHandler(String key) { + IPanelHandler panelHandler = findPanelHandlerNullable(key); + if (panelHandler == null) { + throw new NoSuchElementException("Expected to find panel sync handler with key '" + key + "', but none was found."); + } + return panelHandler; + } + public PanelSyncManager registerSlotGroup(SlotGroup slotGroup) { if (!slotGroup.isSingleton()) { this.slotGroups.put(slotGroup.getName(), slotGroup); From 366d13da074510d0fdbf536cd955e7e696247b3b Mon Sep 17 00:00:00 2001 From: brachy <45517902+brachy84@users.noreply.github.com> Date: Thu, 4 Dec 2025 19:24:22 +0100 Subject: [PATCH 07/50] Rework sync handler and value validation (#181) * rework * javadoc n stuff * non extendable (cherry picked from commit cf3462dc2505162636e2aea157b572eb63a5e3ef) --- .../modularui/api/value/ISyncOrValue.java | 120 +++++++++++++ .../modularui/api/value/IValue.java | 19 +- .../modularui/api/widget/ISynced.java | 36 +++- .../modularui/screen/ModularPanel.java | 18 +- .../utils/serialization/ByteBufAdapters.java | 6 +- .../modularui/value/BoolValue.java | 10 ++ .../modularui/value/ByteValue.java | 5 + .../modularui/value/ConstValue.java | 5 + .../modularui/value/DoubleValue.java | 10 ++ .../modularui/value/DynamicValue.java | 5 + .../modularui/value/EnumValue.java | 10 ++ .../modularui/value/FloatValue.java | 10 ++ .../cleanroommc/modularui/value/IntValue.java | 10 ++ .../modularui/value/LongValue.java | 10 ++ .../modularui/value/ObjectValue.java | 31 ++++ .../modularui/value/StringValue.java | 4 +- .../value/sync/AbstractGenericSyncValue.java | 143 +++++++++++++++ .../value/sync/BigDecimalSyncValue.java | 2 +- .../modularui/value/sync/BigIntSyncValue.java | 2 +- .../value/sync/BooleanSyncValue.java | 5 + .../value/sync/ByteArraySyncValue.java | 2 +- .../modularui/value/sync/ByteSyncValue.java | 5 + .../modularui/value/sync/DoubleSyncValue.java | 5 + .../modularui/value/sync/EnumSyncValue.java | 5 + .../modularui/value/sync/FloatSyncValue.java | 5 + .../value/sync/FluidSlotSyncHandler.java | 5 + .../value/sync/GenericListSyncHandler.java | 5 + .../value/sync/GenericMapSyncHandler.java | 5 + .../value/sync/GenericSetSyncHandler.java | 5 + .../value/sync/GenericSyncValue.java | 167 ++++++++++++------ .../modularui/value/sync/IntSyncValue.java | 5 + .../value/sync/LongArraySyncValue.java | 2 +- .../modularui/value/sync/LongSyncValue.java | 5 + .../value/sync/PanelSyncHandler.java | 2 +- .../modularui/value/sync/StringSyncValue.java | 72 ++------ .../modularui/value/sync/SyncHandler.java | 11 +- .../modularui/value/sync/SyncHandlers.java | 7 +- .../cleanroommc/modularui/widget/Widget.java | 22 ++- .../widgets/AbstractCycleButtonWidget.java | 34 ++-- .../modularui/widgets/ButtonWidget.java | 25 ++- .../widgets/DynamicSyncedWidget.java | 15 +- .../modularui/widgets/Expandable.java | 2 - .../modularui/widgets/ItemDisplayWidget.java | 28 +-- .../modularui/widgets/ProgressWidget.java | 28 +-- .../modularui/widgets/SliderWidget.java | 27 +-- .../modularui/widgets/slot/FluidSlot.java | 14 +- .../modularui/widgets/slot/ItemSlot.java | 15 +- .../modularui/widgets/slot/ModularSlot.java | 8 + .../widgets/slot/PhantomItemSlot.java | 14 +- .../widgets/textfield/TextFieldWidget.java | 26 +-- 50 files changed, 747 insertions(+), 285 deletions(-) create mode 100644 src/main/java/com/cleanroommc/modularui/api/value/ISyncOrValue.java create mode 100644 src/main/java/com/cleanroommc/modularui/value/sync/AbstractGenericSyncValue.java diff --git a/src/main/java/com/cleanroommc/modularui/api/value/ISyncOrValue.java b/src/main/java/com/cleanroommc/modularui/api/value/ISyncOrValue.java new file mode 100644 index 000000000..cc1d8f4ff --- /dev/null +++ b/src/main/java/com/cleanroommc/modularui/api/value/ISyncOrValue.java @@ -0,0 +1,120 @@ +package com.cleanroommc.modularui.api.value; + +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * An interface that is implemented on {@link IValue} and {@link com.cleanroommc.modularui.value.sync.SyncHandler SyncHandler} for easier + * validation and setters. + */ +@ApiStatus.NonExtendable +public interface ISyncOrValue { + + /** + * A sync handler or value representing null. + */ + ISyncOrValue EMPTY = new ISyncOrValue() { + @Override + public @Nullable T castNullable(Class type) { + return null; + } + + @Override + public boolean isTypeOrEmpty(Class type) { + return true; + } + }; + + /** + * Returns the given sync handler or value or {@link #EMPTY} if null. + * + * @param syncOrValue sync handler or value + * @return a non-null representation of the given sync handler or value + */ + @NotNull + static ISyncOrValue orEmpty(@Nullable ISyncOrValue syncOrValue) { + return syncOrValue != null ? syncOrValue : EMPTY; + } + + /** + * Returns if this sync handler or value is an instance of the given type or if this represents null. This is useful, when the value or + * sync handler can be null in the widget. + * + * @param type type to check for + * @return if this sync handler or value is an instance of the type or empty + */ + default boolean isTypeOrEmpty(Class type) { + return type.isAssignableFrom(getClass()); + } + + /** + * Casts this sync handler or value to the given type or null if this isn't a subtype of the given type. + * + * @param type type to cast this sync handle or value to + * @param type to cast to + * @return this cast sync handler or value + */ + @Nullable + @SuppressWarnings("unchecked") + default T castNullable(Class type) { + return type.isAssignableFrom(getClass()) ? (T) this : null; + } + + /** + * Casts this sync handler or value to a {@link IValue IValue<V>} if it is a value handler and the containing value is of type + * {@link V} else null. + * + * @param valueType expected type of the containing value + * @param expected type of the containing value + * @return a {@link IValue IValue<V>} if types match or null + */ + @Nullable + default IValue castValueNullable(Class valueType) { + return null; + } + + /** + * Casts this sync handler or value to the given type or throws an exception if this isn't a subtype of the given type. + * + * @param type type to cast this sync handle or value to + * @param type to cast to + * @return this cast sync handler or value + * @throws IllegalStateException if this is not a subtype of the given type + */ + default T castOrThrow(Class type) { + T t = castNullable(type); + if (t == null) { + if (!isSyncHandler() && !isValueHandler()) { + throw new IllegalStateException("Empty sync handler or value can't be used for anything."); + } + String self = isSyncHandler() ? "sync handler" : "value"; + throw new IllegalStateException("Can't cast " + self + " of type '" + getClass().getSimpleName() + "' to type '" + type.getSimpleName() + "'."); + } + return t; + } + + /** + * Returns if the containing value of this is of the given type. If this is not a value it will always return false. + * + * @param type expected value type + * @return if the containing value of this is of the given type + */ + default boolean isValueOfType(Class type) { + return false; + } + + /** + * @return if this is a sync handler (false if this represents null) + */ + default boolean isSyncHandler() { + return false; + } + + /** + * @return if this is a value handler (false if this represents null) + */ + default boolean isValueHandler() { + return false; + } +} diff --git a/src/main/java/com/cleanroommc/modularui/api/value/IValue.java b/src/main/java/com/cleanroommc/modularui/api/value/IValue.java index ff164c7f2..a1a8ff299 100644 --- a/src/main/java/com/cleanroommc/modularui/api/value/IValue.java +++ b/src/main/java/com/cleanroommc/modularui/api/value/IValue.java @@ -5,7 +5,7 @@ * * @param value type */ -public interface IValue { +public interface IValue extends ISyncOrValue { /** * Gets the current value. @@ -20,4 +20,21 @@ public interface IValue { * @param value new value */ void setValue(T value); + + Class getValueType(); + + default boolean isValueOfType(Class type) { + return type.isAssignableFrom(getValueType()); + } + + @SuppressWarnings("unchecked") + @Override + default IValue castValueNullable(Class valueType) { + return isValueOfType(valueType) ? (IValue) this : null; + } + + @Override + default boolean isValueHandler() { + return true; + } } diff --git a/src/main/java/com/cleanroommc/modularui/api/widget/ISynced.java b/src/main/java/com/cleanroommc/modularui/api/widget/ISynced.java index 2e2543595..206f12f0f 100644 --- a/src/main/java/com/cleanroommc/modularui/api/widget/ISynced.java +++ b/src/main/java/com/cleanroommc/modularui/api/widget/ISynced.java @@ -1,5 +1,6 @@ package com.cleanroommc.modularui.api.widget; +import com.cleanroommc.modularui.api.value.ISyncOrValue; import com.cleanroommc.modularui.value.sync.GenericSyncValue; import com.cleanroommc.modularui.value.sync.ModularSyncManager; import com.cleanroommc.modularui.value.sync.SyncHandler; @@ -29,21 +30,32 @@ default W getThis() { * Called when this widget gets initialised or when this widget is added to the gui * * @param syncManager sync manager - * @param late + * @param late if this is called at any point after the panel this widget belongs to opened */ void initialiseSyncHandler(ModularSyncManager syncManager, boolean late); /** - * Checks and return if the received sync handler is valid for this widget This is usually an instanceof check.
- * Synced widgets must override this! - * - * @param syncHandler received sync handler - * @return true if sync handler is valid + * @deprecated use {@link #isValidSyncOrValue(ISyncOrValue)} */ + @ApiStatus.ScheduledForRemoval(inVersion = "3.2.0") + @Deprecated default boolean isValidSyncHandler(SyncHandler syncHandler) { return false; } + /** + * Returns if the given value or sync handler is valid for this widget. This is usually a call to + * {@link ISyncOrValue#isTypeOrEmpty(Class)}. If the widget must specify a value (disallow null) instanceof check can be used. You can + * check for primitive types which don't have a dedicated {@link com.cleanroommc.modularui.api.value.IValue IValue} interface with + * {@link ISyncOrValue#isValueOfType(Class)}. + * + * @param syncOrValue a sync handler or a value, but never null + * @return if the value or sync handler is valid for this class + */ + default boolean isValidSyncOrValue(@NotNull ISyncOrValue syncOrValue) { + return !(syncOrValue instanceof SyncHandler syncHandler) || isValidSyncHandler(syncHandler); + } + /** * Checks if the given sync handler is valid for this widget and throws an exception if not. * Override {@link #isValidSyncHandler(SyncHandler)} @@ -52,17 +64,21 @@ default boolean isValidSyncHandler(SyncHandler syncHandler) { * @throws IllegalStateException if the given sync handler is invalid for this widget. */ @ApiStatus.NonExtendable - default void checkValidSyncHandler(SyncHandler syncHandler) { - if (!isValidSyncHandler(syncHandler)) { + default void checkValidSyncOrValue(ISyncOrValue syncHandler) { + if (!isValidSyncOrValue(syncHandler)) { throw new IllegalStateException("SyncHandler of type '" + syncHandler.getClass().getSimpleName() + "' is not valid " + "for widget '" + this + "'."); } } + @ApiStatus.ScheduledForRemoval(inVersion = "3.2.0") + @Deprecated default T castIfTypeElseNull(SyncHandler syncHandler, Class clazz) { return castIfTypeElseNull(syncHandler, clazz, null); } + @ApiStatus.ScheduledForRemoval(inVersion = "3.2.0") + @Deprecated @SuppressWarnings("unchecked") default T castIfTypeElseNull(SyncHandler syncHandler, Class clazz, @Nullable Consumer setup) { if (syncHandler != null && clazz.isAssignableFrom(syncHandler.getClass())) { @@ -73,10 +89,14 @@ default T castIfTypeElseNull(SyncHandler syncHandler, Class clazz, @Nulla return null; } + @ApiStatus.ScheduledForRemoval(inVersion = "3.2.0") + @Deprecated default GenericSyncValue castIfTypeGenericElseNull(SyncHandler syncHandler, Class clazz) { return castIfTypeGenericElseNull(syncHandler, clazz, null); } + @ApiStatus.ScheduledForRemoval(inVersion = "3.2.0") + @Deprecated default GenericSyncValue castIfTypeGenericElseNull(SyncHandler syncHandler, Class clazz, @Nullable Consumer> setup) { if (syncHandler instanceof GenericSyncValue genericSyncValue && genericSyncValue.isOfType(clazz)) { diff --git a/src/main/java/com/cleanroommc/modularui/screen/ModularPanel.java b/src/main/java/com/cleanroommc/modularui/screen/ModularPanel.java index 5bc31d23b..724e5ae98 100644 --- a/src/main/java/com/cleanroommc/modularui/screen/ModularPanel.java +++ b/src/main/java/com/cleanroommc/modularui/screen/ModularPanel.java @@ -8,6 +8,7 @@ import com.cleanroommc.modularui.api.UpOrDown; import com.cleanroommc.modularui.api.layout.IViewport; import com.cleanroommc.modularui.api.layout.IViewportStack; +import com.cleanroommc.modularui.api.value.ISyncOrValue; import com.cleanroommc.modularui.api.widget.IDragResizeable; import com.cleanroommc.modularui.api.widget.IFocusedWidget; import com.cleanroommc.modularui.api.widget.IWidget; @@ -25,7 +26,6 @@ import com.cleanroommc.modularui.utils.ObjectList; import com.cleanroommc.modularui.value.sync.PanelSyncHandler; import com.cleanroommc.modularui.value.sync.PanelSyncManager; -import com.cleanroommc.modularui.value.sync.SyncHandler; import com.cleanroommc.modularui.widget.ParentWidget; import com.cleanroommc.modularui.widget.WidgetTree; import com.cleanroommc.modularui.widget.sizer.Area; @@ -113,15 +113,19 @@ public void onInit() { } @Override - public boolean isValidSyncHandler(SyncHandler syncHandler) { - return syncHandler instanceof IPanelHandler; + public boolean isValidSyncOrValue(@NotNull ISyncOrValue syncOrValue) { + return syncOrValue.isTypeOrEmpty(IPanelHandler.class); } - @ApiStatus.Internal @Override - public void setSyncHandler(@Nullable SyncHandler syncHandler) { - super.setSyncHandler(syncHandler); - setPanelHandler((IPanelHandler) syncHandler); + protected void setSyncOrValue(@NotNull ISyncOrValue syncOrValue) { + super.setSyncOrValue(syncOrValue); + setPanelHandler(syncOrValue.castNullable(IPanelHandler.class)); + } + + @ApiStatus.Internal + public void setPanelSyncHandler(PanelSyncHandler syncHandler) { + setSyncOrValue(ISyncOrValue.orEmpty(syncHandler)); } /** diff --git a/src/main/java/com/cleanroommc/modularui/utils/serialization/ByteBufAdapters.java b/src/main/java/com/cleanroommc/modularui/utils/serialization/ByteBufAdapters.java index 22e52bdca..03d7d6174 100644 --- a/src/main/java/com/cleanroommc/modularui/utils/serialization/ByteBufAdapters.java +++ b/src/main/java/com/cleanroommc/modularui/utils/serialization/ByteBufAdapters.java @@ -14,6 +14,7 @@ import java.io.IOException; import java.math.BigDecimal; import java.math.BigInteger; +import java.util.Objects; public class ByteBufAdapters { @@ -111,8 +112,7 @@ public boolean areEqual(@NotNull BigDecimal t1, @NotNull BigDecimal t2) { } }; - public static IByteBufAdapter makeAdapter(@NotNull IByteBufDeserializer deserializer, @NotNull IByteBufSerializer serializer, @Nullable IEquals comparator) { - final IEquals tester = comparator != null ? comparator : IEquals.defaultTester(); + public static IByteBufAdapter makeAdapter(@NotNull IByteBufDeserializer deserializer, @NotNull IByteBufSerializer serializer, @Nullable IEquals tester) { return new IByteBufAdapter<>() { @Override public T deserialize(PacketBuffer buffer) throws IOException { @@ -126,7 +126,7 @@ public void serialize(PacketBuffer buffer, T u) throws IOException { @Override public boolean areEqual(@NotNull T t1, @NotNull T t2) { - return tester.areEqual(t1, t2); + return tester != null ? tester.areEqual(t1, t2) : Objects.equals(t1, t2); } }; } diff --git a/src/main/java/com/cleanroommc/modularui/value/BoolValue.java b/src/main/java/com/cleanroommc/modularui/value/BoolValue.java index 9feb5c44e..75744b8ee 100644 --- a/src/main/java/com/cleanroommc/modularui/value/BoolValue.java +++ b/src/main/java/com/cleanroommc/modularui/value/BoolValue.java @@ -54,6 +54,11 @@ public void setStringValue(String val) { setBoolValue(Boolean.parseBoolean(val)); } + @Override + public Class getValueType() { + return Boolean.class; + } + public static class Dynamic implements IBoolValue, IIntValue, IStringValue { private final BooleanSupplier getter; @@ -103,5 +108,10 @@ public int getIntValue() { public void setIntValue(int val) { setBoolValue(val == 1); } + + @Override + public Class getValueType() { + return Boolean.class; + } } } diff --git a/src/main/java/com/cleanroommc/modularui/value/ByteValue.java b/src/main/java/com/cleanroommc/modularui/value/ByteValue.java index c7a975642..28e217242 100644 --- a/src/main/java/com/cleanroommc/modularui/value/ByteValue.java +++ b/src/main/java/com/cleanroommc/modularui/value/ByteValue.java @@ -30,6 +30,11 @@ public void setValue(Byte value) { setByteValue(value); } + @Override + public Class getValueType() { + return Byte.class; + } + public static class Dynamic extends ByteValue { private final Supplier getter; diff --git a/src/main/java/com/cleanroommc/modularui/value/ConstValue.java b/src/main/java/com/cleanroommc/modularui/value/ConstValue.java index 90bd99a73..a5bd4e94b 100644 --- a/src/main/java/com/cleanroommc/modularui/value/ConstValue.java +++ b/src/main/java/com/cleanroommc/modularui/value/ConstValue.java @@ -26,4 +26,9 @@ public T getValue() { public void setValue(T value) { this.value = value; } + + @Override + public Class getValueType() { + return (Class) value.getClass(); + } } diff --git a/src/main/java/com/cleanroommc/modularui/value/DoubleValue.java b/src/main/java/com/cleanroommc/modularui/value/DoubleValue.java index 8a6ba1c69..9f28c0465 100644 --- a/src/main/java/com/cleanroommc/modularui/value/DoubleValue.java +++ b/src/main/java/com/cleanroommc/modularui/value/DoubleValue.java @@ -65,6 +65,11 @@ public void setFloatValue(float val) { setDoubleValue(val); } + @Override + public Class getValueType() { + return Double.class; + } + public static class Dynamic implements IDoubleValue, IStringValue { private final DoubleSupplier getter; @@ -104,5 +109,10 @@ public Double getValue() { public void setValue(Double value) { setDoubleValue(value); } + + @Override + public Class getValueType() { + return Double.class; + } } } diff --git a/src/main/java/com/cleanroommc/modularui/value/DynamicValue.java b/src/main/java/com/cleanroommc/modularui/value/DynamicValue.java index aa7ae620f..416c312d8 100644 --- a/src/main/java/com/cleanroommc/modularui/value/DynamicValue.java +++ b/src/main/java/com/cleanroommc/modularui/value/DynamicValue.java @@ -35,4 +35,9 @@ public void setValue(T value) { this.setter.accept(value); } } + + @Override + public Class getValueType() { + return (Class) this.getter.get().getClass(); + } } diff --git a/src/main/java/com/cleanroommc/modularui/value/EnumValue.java b/src/main/java/com/cleanroommc/modularui/value/EnumValue.java index a57a9ec78..d9117c05b 100644 --- a/src/main/java/com/cleanroommc/modularui/value/EnumValue.java +++ b/src/main/java/com/cleanroommc/modularui/value/EnumValue.java @@ -45,6 +45,11 @@ public Class getEnumClass() { return this.enumClass; } + @Override + public Class getValueType() { + return this.enumClass; + } + public static class Dynamic> implements IEnumValue, IIntValue { protected final Class enumClass; @@ -81,5 +86,10 @@ public void setValue(T value) { public Class getEnumClass() { return this.enumClass; } + + @Override + public Class getValueType() { + return this.enumClass; + } } } diff --git a/src/main/java/com/cleanroommc/modularui/value/FloatValue.java b/src/main/java/com/cleanroommc/modularui/value/FloatValue.java index 85b4b9c5c..5b04ea311 100644 --- a/src/main/java/com/cleanroommc/modularui/value/FloatValue.java +++ b/src/main/java/com/cleanroommc/modularui/value/FloatValue.java @@ -64,6 +64,11 @@ public void setStringValue(String val) { setFloatValue(Float.parseFloat(val)); } + @Override + public Class getValueType() { + return Float.class; + } + public static class Dynamic implements IFloatValue, IDoubleValue, IStringValue { private final FloatSupplier getter; @@ -113,5 +118,10 @@ public double getDoubleValue() { public void setDoubleValue(double val) { setFloatValue((float) val); } + + @Override + public Class getValueType() { + return Float.class; + } } } diff --git a/src/main/java/com/cleanroommc/modularui/value/IntValue.java b/src/main/java/com/cleanroommc/modularui/value/IntValue.java index 5f8faba69..a594ffa5d 100644 --- a/src/main/java/com/cleanroommc/modularui/value/IntValue.java +++ b/src/main/java/com/cleanroommc/modularui/value/IntValue.java @@ -64,6 +64,11 @@ public void setStringValue(String val) { setIntValue(Integer.parseInt(val)); } + @Override + public Class getValueType() { + return Integer.class; + } + public static class Dynamic implements IIntValue, IStringValue { private final IntSupplier getter; @@ -103,5 +108,10 @@ public Integer getValue() { public void setValue(Integer value) { setIntValue(value); } + + @Override + public Class getValueType() { + return Integer.class; + } } } diff --git a/src/main/java/com/cleanroommc/modularui/value/LongValue.java b/src/main/java/com/cleanroommc/modularui/value/LongValue.java index dc7b161fd..0eececdce 100644 --- a/src/main/java/com/cleanroommc/modularui/value/LongValue.java +++ b/src/main/java/com/cleanroommc/modularui/value/LongValue.java @@ -64,6 +64,11 @@ public void setIntValue(int val) { setLongValue(val); } + @Override + public Class getValueType() { + return Long.class; + } + public static class Dynamic implements ILongValue, IIntValue, IStringValue { private final LongSupplier getter; @@ -113,5 +118,10 @@ public int getIntValue() { public void setIntValue(int val) { setLongValue(val); } + + @Override + public Class getValueType() { + return Long.class; + } } } diff --git a/src/main/java/com/cleanroommc/modularui/value/ObjectValue.java b/src/main/java/com/cleanroommc/modularui/value/ObjectValue.java index d089c9313..a87840c26 100644 --- a/src/main/java/com/cleanroommc/modularui/value/ObjectValue.java +++ b/src/main/java/com/cleanroommc/modularui/value/ObjectValue.java @@ -3,6 +3,7 @@ import com.cleanroommc.modularui.api.value.IValue; import com.google.common.util.concurrent.AtomicDouble; +import org.jetbrains.annotations.ApiStatus; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; @@ -18,9 +19,18 @@ public static Dynamic wrapAtomic(AtomicReference val) { return new Dynamic<>(val::get, val::set); } + private final Class type; private T value; + public ObjectValue(Class type, T value) { + this.type = type; + this.value = value; + } + + @ApiStatus.ScheduledForRemoval(inVersion = "3.2.0") + @Deprecated public ObjectValue(T value) { + this.type = value != null ? (Class) value.getClass() : null; this.value = value; } @@ -34,14 +44,30 @@ public void setValue(T value) { this.value = value; } + @Override + public Class getValueType() { + return this.type != null ? this.type : (Class) this.value.getClass(); + } + public static class Dynamic implements IValue { + private final Class type; private final Supplier getter; private final Consumer setter; + public Dynamic(Class type, Supplier getter, Consumer setter) { + this.type = type; + this.getter = getter; + this.setter = setter; + } + + @ApiStatus.ScheduledForRemoval(inVersion = "3.2.0") + @Deprecated public Dynamic(Supplier getter, Consumer setter) { this.getter = getter; this.setter = setter; + T value = getter.get(); + this.type = value != null ? (Class) value.getClass() : null; } @Override @@ -53,5 +79,10 @@ public T getValue() { public void setValue(T value) { this.setter.accept(value); } + + @Override + public Class getValueType() { + return this.type != null ? this.type : (Class) this.getter.get().getClass(); + } } } diff --git a/src/main/java/com/cleanroommc/modularui/value/StringValue.java b/src/main/java/com/cleanroommc/modularui/value/StringValue.java index 30808995b..67bd94052 100644 --- a/src/main/java/com/cleanroommc/modularui/value/StringValue.java +++ b/src/main/java/com/cleanroommc/modularui/value/StringValue.java @@ -14,7 +14,7 @@ public static Dynamic wrap(IStringValue val) { } public StringValue(String value) { - super(value); + super(String.class, value); } @Override @@ -30,7 +30,7 @@ public void setStringValue(String val) { public static class Dynamic extends ObjectValue.Dynamic implements IStringValue { public Dynamic(Supplier getter, @Nullable Consumer setter) { - super(getter, setter); + super(String.class, getter, setter); } @Override diff --git a/src/main/java/com/cleanroommc/modularui/value/sync/AbstractGenericSyncValue.java b/src/main/java/com/cleanroommc/modularui/value/sync/AbstractGenericSyncValue.java new file mode 100644 index 000000000..687aa028e --- /dev/null +++ b/src/main/java/com/cleanroommc/modularui/value/sync/AbstractGenericSyncValue.java @@ -0,0 +1,143 @@ +package com.cleanroommc.modularui.value.sync; + +import com.cleanroommc.modularui.network.NetworkUtils; + +import net.minecraft.network.PacketBuffer; + +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.Nullable; + +import java.io.IOException; +import java.util.Objects; +import java.util.function.Consumer; +import java.util.function.Supplier; + +public abstract class AbstractGenericSyncValue extends ValueSyncHandler { + + private final Class type; + private final Supplier getter; + private final Consumer setter; + private T cache; + + protected AbstractGenericSyncValue(Class type, Supplier getter, Consumer setter) { + this.getter = Objects.requireNonNull(getter); + this.setter = setter; + this.cache = getter.get(); + if (type == null && this.cache != null) { + type = (Class) this.cache.getClass(); + } + this.type = type; + } + + @Contract("_, null, _, null, _ -> fail") + protected AbstractGenericSyncValue(Class type, @Nullable Supplier clientGetter, @Nullable Consumer clientSetter, + @Nullable Supplier serverGetter, @Nullable Consumer serverSetter) { + if (clientGetter == null && serverGetter == null) { + throw new NullPointerException("Client or server getter must not be null!"); + } + if (NetworkUtils.isClient()) { + this.getter = clientGetter != null ? clientGetter : serverGetter; + this.setter = clientSetter != null ? clientSetter : serverSetter; + } else { + this.getter = serverGetter != null ? serverGetter : clientGetter; + this.setter = serverSetter != null ? serverSetter : clientSetter; + } + this.cache = this.getter.get(); + if (type == null && this.cache != null) { + type = (Class) this.cache.getClass(); + } + this.type = type; + } + + protected abstract T createDeepCopyOf(T value); + + protected abstract boolean areEqual(T a, T b); + + protected abstract void serialize(PacketBuffer buffer, T value) throws IOException; + + protected abstract T deserialize(PacketBuffer buffer) throws IOException; + + @Override + public T getValue() { + return this.cache; + } + + @Override + public void setValue(T value, boolean setSource, boolean sync) { + this.cache = createDeepCopyOf(value); + if (setSource && this.setter != null) { + this.setter.accept(value); + } + onValueChanged(); + if (sync) sync(); + } + + @Override + public boolean updateCacheFromSource(boolean isFirstSync) { + if (this.getter == null) return false; + T t = this.getter.get(); + if (isFirstSync || !areEqual(this.cache, t)) { + setValue(t, false, false); + return true; + } + return false; + } + + @Override + public void notifyUpdate() { + if (this.getter == null) throw new NullPointerException("Can't notify sync handler with null getter."); + setValue(this.getter.get(), false, true); + } + + @Override + public void write(PacketBuffer buffer) throws IOException { + serialize(buffer, this.cache); + } + + @Override + public void read(PacketBuffer buffer) throws IOException { + setValue(deserialize(buffer), true, false); + } + + @SuppressWarnings("unchecked") + @Override + public Class getValueType() { + return (Class) getType(); + } + + @ApiStatus.ScheduledForRemoval(inVersion = "3.2.0") + @Deprecated + @SuppressWarnings("unchecked") + public @Nullable Class getType() { + if (this.type != null) return type; + if (this.cache != null) { + return (Class) this.cache.getClass(); + } + T t = this.getter.get(); + if (t != null) { + return (Class) t.getClass(); + } + return null; + } + + @ApiStatus.ScheduledForRemoval(inVersion = "3.2.0") + @Deprecated + public boolean isOfType(Class expectedType) { + return isValueOfType(expectedType); + } + + @Override + public boolean isValueOfType(Class expectedType) { + Class type = getValueType(); + if (type == null) { + throw new IllegalStateException("Could not infer type of GenericSyncValue since value is null!"); + } + return expectedType.isAssignableFrom(type); + } + + @SuppressWarnings("unchecked") + public AbstractGenericSyncValue cast() { + return (AbstractGenericSyncValue) this; + } +} diff --git a/src/main/java/com/cleanroommc/modularui/value/sync/BigDecimalSyncValue.java b/src/main/java/com/cleanroommc/modularui/value/sync/BigDecimalSyncValue.java index ef4091c42..e3d2a2573 100644 --- a/src/main/java/com/cleanroommc/modularui/value/sync/BigDecimalSyncValue.java +++ b/src/main/java/com/cleanroommc/modularui/value/sync/BigDecimalSyncValue.java @@ -14,7 +14,7 @@ public class BigDecimalSyncValue extends GenericSyncValue implements IStringValue { public BigDecimalSyncValue(@NotNull Supplier getter, @Nullable Consumer setter) { - super(getter, setter, ByteBufAdapters.BIG_DECIMAL, ICopy.immutable()); + super(BigDecimal.class, getter, setter, ByteBufAdapters.BIG_DECIMAL, ICopy.immutable()); } @Override diff --git a/src/main/java/com/cleanroommc/modularui/value/sync/BigIntSyncValue.java b/src/main/java/com/cleanroommc/modularui/value/sync/BigIntSyncValue.java index fecb3fba9..7a1f9a48f 100644 --- a/src/main/java/com/cleanroommc/modularui/value/sync/BigIntSyncValue.java +++ b/src/main/java/com/cleanroommc/modularui/value/sync/BigIntSyncValue.java @@ -14,7 +14,7 @@ public class BigIntSyncValue extends GenericSyncValue implements IStringValue { public BigIntSyncValue(@NotNull Supplier getter, @Nullable Consumer setter) { - super(getter, setter, ByteBufAdapters.BIG_INT, ICopy.immutable()); + super(BigInteger.class, getter, setter, ByteBufAdapters.BIG_INT, ICopy.immutable()); } @Override diff --git a/src/main/java/com/cleanroommc/modularui/value/sync/BooleanSyncValue.java b/src/main/java/com/cleanroommc/modularui/value/sync/BooleanSyncValue.java index 4f5020f47..74f7c40f9 100644 --- a/src/main/java/com/cleanroommc/modularui/value/sync/BooleanSyncValue.java +++ b/src/main/java/com/cleanroommc/modularui/value/sync/BooleanSyncValue.java @@ -110,4 +110,9 @@ public void setStringValue(String value, boolean setSource, boolean sync) { public String getStringValue() { return String.valueOf(this.cache); } + + @Override + public Class getValueType() { + return Boolean.class; + } } diff --git a/src/main/java/com/cleanroommc/modularui/value/sync/ByteArraySyncValue.java b/src/main/java/com/cleanroommc/modularui/value/sync/ByteArraySyncValue.java index c56168887..7d900fe34 100644 --- a/src/main/java/com/cleanroommc/modularui/value/sync/ByteArraySyncValue.java +++ b/src/main/java/com/cleanroommc/modularui/value/sync/ByteArraySyncValue.java @@ -11,6 +11,6 @@ public class ByteArraySyncValue extends GenericSyncValue { public ByteArraySyncValue(@NotNull Supplier getter, @Nullable Consumer setter) { - super(getter, setter, ByteBufAdapters.BYTE_ARR, byte[]::clone); + super(byte[].class, getter, setter, ByteBufAdapters.BYTE_ARR, byte[]::clone); } } diff --git a/src/main/java/com/cleanroommc/modularui/value/sync/ByteSyncValue.java b/src/main/java/com/cleanroommc/modularui/value/sync/ByteSyncValue.java index c7385a0b0..c89352484 100644 --- a/src/main/java/com/cleanroommc/modularui/value/sync/ByteSyncValue.java +++ b/src/main/java/com/cleanroommc/modularui/value/sync/ByteSyncValue.java @@ -97,4 +97,9 @@ public void setByteValue(byte value, boolean setSource, boolean sync) { public byte getByteValue() { return this.cache; } + + @Override + public Class getValueType() { + return Byte.class; + } } diff --git a/src/main/java/com/cleanroommc/modularui/value/sync/DoubleSyncValue.java b/src/main/java/com/cleanroommc/modularui/value/sync/DoubleSyncValue.java index ca78ff139..d7465216d 100644 --- a/src/main/java/com/cleanroommc/modularui/value/sync/DoubleSyncValue.java +++ b/src/main/java/com/cleanroommc/modularui/value/sync/DoubleSyncValue.java @@ -121,4 +121,9 @@ public float getFloatValue() { public void setFloatValue(float value, boolean setSource, boolean sync) { setDoubleValue(value, setSource, sync); } + + @Override + public Class getValueType() { + return Double.class; + } } diff --git a/src/main/java/com/cleanroommc/modularui/value/sync/EnumSyncValue.java b/src/main/java/com/cleanroommc/modularui/value/sync/EnumSyncValue.java index baf6b2d8c..a65ddeb24 100644 --- a/src/main/java/com/cleanroommc/modularui/value/sync/EnumSyncValue.java +++ b/src/main/java/com/cleanroommc/modularui/value/sync/EnumSyncValue.java @@ -109,4 +109,9 @@ public void setIntValue(int value, boolean setSource, boolean sync) { public int getIntValue() { return this.cache.ordinal(); } + + @Override + public Class getValueType() { + return this.enumClass; + } } diff --git a/src/main/java/com/cleanroommc/modularui/value/sync/FloatSyncValue.java b/src/main/java/com/cleanroommc/modularui/value/sync/FloatSyncValue.java index 9501bd029..00387b8c5 100644 --- a/src/main/java/com/cleanroommc/modularui/value/sync/FloatSyncValue.java +++ b/src/main/java/com/cleanroommc/modularui/value/sync/FloatSyncValue.java @@ -121,4 +121,9 @@ public double getDoubleValue() { public void setDoubleValue(double value, boolean setSource, boolean sync) { setFloatValue((float) value, setSource, sync); } + + @Override + public Class getValueType() { + return Float.class; + } } diff --git a/src/main/java/com/cleanroommc/modularui/value/sync/FluidSlotSyncHandler.java b/src/main/java/com/cleanroommc/modularui/value/sync/FluidSlotSyncHandler.java index 12a4f36db..80a5239ec 100644 --- a/src/main/java/com/cleanroommc/modularui/value/sync/FluidSlotSyncHandler.java +++ b/src/main/java/com/cleanroommc/modularui/value/sync/FluidSlotSyncHandler.java @@ -77,6 +77,11 @@ public boolean updateCacheFromSource(boolean isFirstSync) { return false; } + @Override + public Class getValueType() { + return FluidStack.class; + } + @Override public void notifyUpdate() { setValue(this.fluidTank.getFluid(), false, true); diff --git a/src/main/java/com/cleanroommc/modularui/value/sync/GenericListSyncHandler.java b/src/main/java/com/cleanroommc/modularui/value/sync/GenericListSyncHandler.java index 5f423e9bf..6c3b3a7a6 100644 --- a/src/main/java/com/cleanroommc/modularui/value/sync/GenericListSyncHandler.java +++ b/src/main/java/com/cleanroommc/modularui/value/sync/GenericListSyncHandler.java @@ -60,6 +60,11 @@ public void read(PacketBuffer buffer) throws IOException { onSetCache(getValue(), true, false); } + @Override + public Class> getValueType() { + return (Class>) (Object) List.class; + } + public static Builder builder() { return new Builder<>(); } diff --git a/src/main/java/com/cleanroommc/modularui/value/sync/GenericMapSyncHandler.java b/src/main/java/com/cleanroommc/modularui/value/sync/GenericMapSyncHandler.java index 6dca9f934..76bbcbbe4 100644 --- a/src/main/java/com/cleanroommc/modularui/value/sync/GenericMapSyncHandler.java +++ b/src/main/java/com/cleanroommc/modularui/value/sync/GenericMapSyncHandler.java @@ -112,6 +112,11 @@ public Map getValue() { return Collections.unmodifiableMap(this.cache); } + @Override + public Class> getValueType() { + return (Class>) (Object) Map.class; + } + public static class Builder { private Supplier> getter; diff --git a/src/main/java/com/cleanroommc/modularui/value/sync/GenericSetSyncHandler.java b/src/main/java/com/cleanroommc/modularui/value/sync/GenericSetSyncHandler.java index 46793593d..6a5725559 100644 --- a/src/main/java/com/cleanroommc/modularui/value/sync/GenericSetSyncHandler.java +++ b/src/main/java/com/cleanroommc/modularui/value/sync/GenericSetSyncHandler.java @@ -54,6 +54,11 @@ public void read(PacketBuffer buffer) throws IOException { onSetCache(getValue(), true, false); } + @Override + public Class> getValueType() { + return (Class>) (Object) Set.class; + } + public static Builder builder() { return new Builder<>(); } diff --git a/src/main/java/com/cleanroommc/modularui/value/sync/GenericSyncValue.java b/src/main/java/com/cleanroommc/modularui/value/sync/GenericSyncValue.java index dbf9711a6..23e6242b2 100644 --- a/src/main/java/com/cleanroommc/modularui/value/sync/GenericSyncValue.java +++ b/src/main/java/com/cleanroommc/modularui/value/sync/GenericSyncValue.java @@ -11,6 +11,7 @@ import net.minecraft.network.PacketBuffer; import net.minecraftforge.fluids.FluidStack; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -19,7 +20,7 @@ import java.util.function.Consumer; import java.util.function.Supplier; -public class GenericSyncValue extends ValueSyncHandler { +public class GenericSyncValue extends AbstractGenericSyncValue { public static GenericSyncValue forItem(@NotNull Supplier getter, @Nullable Consumer setter) { return new GenericSyncValue<>(getter, setter, ByteBufAdapters.ITEM_STACK); @@ -29,20 +30,21 @@ public static GenericSyncValue forFluid(@NotNull Supplier(getter, setter, ByteBufAdapters.FLUID_STACK); } - private final Supplier getter; - private final Consumer setter; private final IByteBufDeserializer deserializer; private final IByteBufSerializer serializer; private final IEquals equals; private final ICopy copy; - private T cache; + @ApiStatus.ScheduledForRemoval(inVersion = "3.2.0") + @Deprecated public GenericSyncValue(@NotNull Supplier getter, @Nullable Consumer setter, @NotNull IByteBufAdapter adapter) { this(getter, setter, adapter, adapter, adapter, null); } + @ApiStatus.ScheduledForRemoval(inVersion = "3.2.0") + @Deprecated public GenericSyncValue(@NotNull Supplier getter, @Nullable Consumer setter, @NotNull IByteBufAdapter adapter, @@ -50,6 +52,8 @@ public GenericSyncValue(@NotNull Supplier getter, this(getter, setter, adapter, adapter, adapter, copy); } + @ApiStatus.ScheduledForRemoval(inVersion = "3.2.0") + @Deprecated public GenericSyncValue(@NotNull Supplier getter, @Nullable Consumer setter, @NotNull IByteBufDeserializer deserializer, @@ -57,6 +61,8 @@ public GenericSyncValue(@NotNull Supplier getter, this(getter, setter, deserializer, serializer, null, null); } + @ApiStatus.ScheduledForRemoval(inVersion = "3.2.0") + @Deprecated public GenericSyncValue(@NotNull Supplier getter, @Nullable Consumer setter, @NotNull IByteBufDeserializer deserializer, @@ -65,23 +71,31 @@ public GenericSyncValue(@NotNull Supplier getter, this(getter, setter, deserializer, serializer, null, copy); } + @ApiStatus.ScheduledForRemoval(inVersion = "3.2.0") + @Deprecated public GenericSyncValue(@NotNull Supplier getter, @NotNull IByteBufAdapter adapter) { this(getter, null, adapter, adapter, adapter, null); } + @ApiStatus.ScheduledForRemoval(inVersion = "3.2.0") + @Deprecated public GenericSyncValue(@NotNull Supplier getter, @NotNull IByteBufAdapter adapter, @Nullable ICopy copy) { this(getter, null, adapter, adapter, adapter, copy); } + @ApiStatus.ScheduledForRemoval(inVersion = "3.2.0") + @Deprecated public GenericSyncValue(@NotNull Supplier getter, @NotNull IByteBufDeserializer deserializer, @NotNull IByteBufSerializer serializer) { this(getter, null, deserializer, serializer, null, null); } + @ApiStatus.ScheduledForRemoval(inVersion = "3.2.0") + @Deprecated public GenericSyncValue(@NotNull Supplier getter, @NotNull IByteBufDeserializer deserializer, @NotNull IByteBufSerializer serializer, @@ -89,15 +103,40 @@ public GenericSyncValue(@NotNull Supplier getter, this(getter, null, deserializer, serializer, null, copy); } + @ApiStatus.ScheduledForRemoval(inVersion = "3.2.0") + @Deprecated public GenericSyncValue(@NotNull Supplier getter, @Nullable Consumer setter, @NotNull IByteBufDeserializer deserializer, @NotNull IByteBufSerializer serializer, @Nullable IEquals equals, @Nullable ICopy copy) { - this.getter = Objects.requireNonNull(getter); - this.cache = getter.get(); - this.setter = setter; + this(null, getter, setter, deserializer, serializer, equals, copy); + } + + public GenericSyncValue(@NotNull Class type, + @NotNull Supplier getter, + @Nullable Consumer setter, + @NotNull IByteBufAdapter adapter, + @Nullable ICopy copy) { + this(type, getter, setter, adapter, adapter, adapter, copy); + } + + public GenericSyncValue(@NotNull Class type, + @NotNull Supplier getter, + @Nullable Consumer setter, + @NotNull IByteBufAdapter adapter) { + this(type, getter, setter, adapter, adapter, adapter, null); + } + + public GenericSyncValue(@NotNull Class type, + @NotNull Supplier getter, + @Nullable Consumer setter, + @NotNull IByteBufDeserializer deserializer, + @NotNull IByteBufSerializer serializer, + @Nullable IEquals equals, + @Nullable ICopy copy) { + super(type, getter, setter); this.deserializer = Objects.requireNonNull(deserializer); this.serializer = Objects.requireNonNull(serializer); this.equals = equals == null ? Objects::equals : IEquals.wrapNullSafe(equals); @@ -105,69 +144,95 @@ public GenericSyncValue(@NotNull Supplier getter, } @Override - public T getValue() { - return this.cache; + protected T createDeepCopyOf(T value) { + return this.copy.createDeepCopy(value); } @Override - public void setValue(T value, boolean setSource, boolean sync) { - this.cache = this.copy.createDeepCopy(value); - if (setSource && this.setter != null) { - this.setter.accept(value); - } - onValueChanged(); - if (sync) sync(); + protected boolean areEqual(T a, T b) { + return this.equals.areEqual(a, b); } @Override - public boolean updateCacheFromSource(boolean isFirstSync) { - if (this.getter == null) return false; - T t = this.getter.get(); - if (isFirstSync || !this.equals.areEqual(this.cache, t)) { - setValue(t, false, false); - return true; - } - return false; + protected void serialize(PacketBuffer buffer, T value) throws IOException { + this.serializer.serialize(buffer, value); } @Override - public void notifyUpdate() { - if (this.getter == null) throw new NullPointerException("Can't notify sync handler with null getter."); - setValue(this.getter.get(), false, true); + protected T deserialize(PacketBuffer buffer) throws IOException { + return this.deserializer.deserialize(buffer); } @Override - public void write(PacketBuffer buffer) throws IOException { - this.serializer.serialize(buffer, this.cache); + @SuppressWarnings("unchecked") + public GenericSyncValue cast() { + return (GenericSyncValue) this; } - @Override - public void read(PacketBuffer buffer) throws IOException { - setValue(this.deserializer.deserialize(buffer), true, false); - } + public static class Builder { - @SuppressWarnings("unchecked") - public @Nullable Class getType() { - if (this.cache != null) { - return (Class) this.cache.getClass(); + private final Class type; + private Supplier getter; + private Consumer setter; + private IByteBufDeserializer deserializer; + private IByteBufSerializer serializer; + private IEquals equals; + private ICopy copy; + + public Builder(Class type) { + this.type = type; } - T t = this.getter.get(); - if (t != null) { - return (Class) t.getClass(); + + public Builder getter(Supplier getter) { + this.getter = getter; + return this; } - return null; - } - public boolean isOfType(Class expectedType) { - Class type = getType(); - if (type == null) { - throw new IllegalStateException("Could not infer type of GenericSyncValue since value is null!"); + public Builder setter(Consumer setter) { + this.setter = setter; + return this; } - return expectedType.isAssignableFrom(type); - } - @SuppressWarnings("unchecked") - public GenericSyncValue cast() { - return (GenericSyncValue) this; + public Builder deserializer(IByteBufDeserializer deserializer) { + this.deserializer = deserializer; + return this; + } + + public Builder serializer(IByteBufSerializer serializer) { + this.serializer = serializer; + return this; + } + + public Builder equals(IEquals equals) { + this.equals = equals; + return this; + } + + public Builder equalsDefault() { + return equals(IEquals.defaultTester()); + } + + public Builder equalsReference() { + return equals((a, b) -> a == b); + } + + public Builder copy(ICopy copy) { + this.copy = copy; + return this; + } + + public Builder copyImmutable() { + return copy(ICopy.immutable()); + } + + public Builder adapter(IByteBufAdapter adapter) { + return deserializer(adapter) + .serializer(adapter) + .equals(adapter); + } + + public GenericSyncValue build() { + return new GenericSyncValue<>(type, getter, setter, deserializer, serializer, equals, copy); + } } } diff --git a/src/main/java/com/cleanroommc/modularui/value/sync/IntSyncValue.java b/src/main/java/com/cleanroommc/modularui/value/sync/IntSyncValue.java index f3144ddc7..50c73487f 100644 --- a/src/main/java/com/cleanroommc/modularui/value/sync/IntSyncValue.java +++ b/src/main/java/com/cleanroommc/modularui/value/sync/IntSyncValue.java @@ -121,4 +121,9 @@ public void setStringValue(String value, boolean setSource, boolean sync) { public String getStringValue() { return String.valueOf(this.cache); } + + @Override + public Class getValueType() { + return Integer.class; + } } diff --git a/src/main/java/com/cleanroommc/modularui/value/sync/LongArraySyncValue.java b/src/main/java/com/cleanroommc/modularui/value/sync/LongArraySyncValue.java index 0278432d6..f970d8a38 100644 --- a/src/main/java/com/cleanroommc/modularui/value/sync/LongArraySyncValue.java +++ b/src/main/java/com/cleanroommc/modularui/value/sync/LongArraySyncValue.java @@ -11,6 +11,6 @@ public class LongArraySyncValue extends GenericSyncValue { public LongArraySyncValue(@NotNull Supplier getter, @Nullable Consumer setter) { - super(getter, setter, ByteBufAdapters.LONG_ARR, long[]::clone); + super(long[].class, getter, setter, ByteBufAdapters.LONG_ARR, long[]::clone); } } diff --git a/src/main/java/com/cleanroommc/modularui/value/sync/LongSyncValue.java b/src/main/java/com/cleanroommc/modularui/value/sync/LongSyncValue.java index 80195ac68..9dd59ad1f 100644 --- a/src/main/java/com/cleanroommc/modularui/value/sync/LongSyncValue.java +++ b/src/main/java/com/cleanroommc/modularui/value/sync/LongSyncValue.java @@ -121,4 +121,9 @@ public int getIntValue() { public String getStringValue() { return String.valueOf(this.cache); } + + @Override + public Class getValueType() { + return Long.class; + } } diff --git a/src/main/java/com/cleanroommc/modularui/value/sync/PanelSyncHandler.java b/src/main/java/com/cleanroommc/modularui/value/sync/PanelSyncHandler.java index 58802bd94..c7a13ae59 100644 --- a/src/main/java/com/cleanroommc/modularui/value/sync/PanelSyncHandler.java +++ b/src/main/java/com/cleanroommc/modularui/value/sync/PanelSyncHandler.java @@ -65,7 +65,7 @@ private void openPanel(boolean syncToServer) { this.syncManager = new PanelSyncManager(client); this.openedPanel = Objects.requireNonNull(createUI(this.syncManager)); this.panelName = this.openedPanel.getName(); - this.openedPanel.setSyncHandler(this); + this.openedPanel.setPanelSyncHandler(this); WidgetTree.collectSyncValues(this.syncManager, this.openedPanel, false); if (!client) { this.openedPanel = null; diff --git a/src/main/java/com/cleanroommc/modularui/value/sync/StringSyncValue.java b/src/main/java/com/cleanroommc/modularui/value/sync/StringSyncValue.java index 3da601895..4ad600bd6 100644 --- a/src/main/java/com/cleanroommc/modularui/value/sync/StringSyncValue.java +++ b/src/main/java/com/cleanroommc/modularui/value/sync/StringSyncValue.java @@ -9,20 +9,15 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import java.io.IOException; import java.util.Objects; import java.util.function.Consumer; import java.util.function.Supplier; -public class StringSyncValue extends ValueSyncHandler implements IStringSyncValue { +public class StringSyncValue extends AbstractGenericSyncValue implements IStringSyncValue { - private final Supplier getter; - private final Consumer setter; - private String cache; - - public StringSyncValue(@NotNull Supplier getter, @Nullable Consumer setter) { - this.getter = Objects.requireNonNull(getter); - this.setter = setter; - this.cache = getter.get(); + public StringSyncValue(Supplier getter, Consumer setter) { + super(String.class, getter, setter); } public StringSyncValue(@NotNull Supplier getter) { @@ -31,72 +26,43 @@ public StringSyncValue(@NotNull Supplier getter) { @Contract("null, null -> fail") public StringSyncValue(@Nullable Supplier clientGetter, - @Nullable Supplier serverGetter) { + @Nullable Supplier serverGetter) { this(clientGetter, null, serverGetter, null); } @Contract("null, _, null, _ -> fail") public StringSyncValue(@Nullable Supplier clientGetter, @Nullable Consumer clientSetter, - @Nullable Supplier serverGetter, @Nullable Consumer serverSetter) { - if (clientGetter == null && serverGetter == null) { - throw new NullPointerException("Client or server getter must not be null!"); - } - if (NetworkUtils.isClient()) { - this.getter = clientGetter != null ? clientGetter : serverGetter; - this.setter = clientSetter != null ? clientSetter : serverSetter; - } else { - this.getter = serverGetter != null ? serverGetter : clientGetter; - this.setter = serverSetter != null ? serverSetter : clientSetter; - } - this.cache = this.getter.get(); - } - - @Override - public String getValue() { - return this.cache; + @Nullable Supplier serverGetter, @Nullable Consumer serverSetter) { + super(String.class, clientGetter, clientSetter, serverGetter, serverSetter); } @Override - public String getStringValue() { - return this.cache; + protected String createDeepCopyOf(String value) { + return value; } @Override - public void setValue(String value, boolean setSource, boolean sync) { - setStringValue(value, setSource, sync); + protected boolean areEqual(String a, String b) { + return Objects.equals(a, b); } @Override - public void setStringValue(String value, boolean setSource, boolean sync) { - this.cache = value; - if (setSource && this.setter != null) { - this.setter.accept(value); - } - onValueChanged(); - if (sync) sync(); + protected void serialize(PacketBuffer buffer, String value) throws IOException { + NetworkUtils.writeStringSafe(buffer, value, Short.MAX_VALUE - 74); } @Override - public boolean updateCacheFromSource(boolean isFirstSync) { - if (isFirstSync || !Objects.equals(this.getter.get(), this.cache)) { - setValue(this.getter.get(), false, false); - return true; - } - return false; + protected String deserialize(PacketBuffer buffer) throws IOException { + return NetworkUtils.readStringSafe(buffer); } @Override - public void notifyUpdate() { - setValue(this.getter.get(), false, true); - } - - @Override - public void write(PacketBuffer buffer) { - NetworkUtils.writeStringSafe(buffer, getValue(), Short.MAX_VALUE - 74); + public String getStringValue() { + return getValue(); } @Override - public void read(PacketBuffer buffer) { - setValue(NetworkUtils.readStringSafe(buffer), true, false); + public void setStringValue(String value, boolean setSource, boolean sync) { + setValue(value, setSource, sync); } } diff --git a/src/main/java/com/cleanroommc/modularui/value/sync/SyncHandler.java b/src/main/java/com/cleanroommc/modularui/value/sync/SyncHandler.java index 5b856ee58..649f7592e 100644 --- a/src/main/java/com/cleanroommc/modularui/value/sync/SyncHandler.java +++ b/src/main/java/com/cleanroommc/modularui/value/sync/SyncHandler.java @@ -1,6 +1,7 @@ package com.cleanroommc.modularui.value.sync; import com.cleanroommc.modularui.api.IPacketWriter; +import com.cleanroommc.modularui.api.value.ISyncOrValue; import com.cleanroommc.modularui.network.NetworkHandler; import com.cleanroommc.modularui.network.packets.PacketSyncHandler; @@ -22,7 +23,7 @@ * A sync handler must exist on client and server. * It must be configured exactly the same to avoid issues. */ -public abstract class SyncHandler { +public abstract class SyncHandler implements ISyncOrValue { private PanelSyncManager syncManager; private String key; @@ -151,8 +152,7 @@ public final void sync(int id) { * * @param init if this method is being called the first time. */ - public void detectAndSendChanges(boolean init) { - } + public void detectAndSendChanges(boolean init) {} /** * @return the key that belongs to this sync handler @@ -178,6 +178,11 @@ public PanelSyncManager getSyncManager() { return this.syncManager; } + @Override + public boolean isSyncHandler() { + return true; + } + public static void sendToClient(String panel, PacketBuffer buffer, SyncHandler syncHandler) { Objects.requireNonNull(buffer); Objects.requireNonNull(syncHandler); diff --git a/src/main/java/com/cleanroommc/modularui/value/sync/SyncHandlers.java b/src/main/java/com/cleanroommc/modularui/value/sync/SyncHandlers.java index 16b69c57d..ce3d30663 100644 --- a/src/main/java/com/cleanroommc/modularui/value/sync/SyncHandlers.java +++ b/src/main/java/com/cleanroommc/modularui/value/sync/SyncHandlers.java @@ -18,8 +18,7 @@ public class SyncHandlers { - private SyncHandlers() { - } + private SyncHandlers() {} public static IntSyncValue intNumber(IntSupplier getter, IntConsumer setter) { return new IntSyncValue(getter, setter); @@ -52,4 +51,8 @@ public static FluidSlotSyncHandler fluidSlot(IFluidTank fluidTank) { public static > EnumSyncValue enumValue(Class clazz, Supplier getter, Consumer setter) { return new EnumSyncValue<>(clazz, getter, setter); } + + public static GenericSyncValue.Builder generic(Class type) { + return new GenericSyncValue.Builder<>(type); + } } diff --git a/src/main/java/com/cleanroommc/modularui/widget/Widget.java b/src/main/java/com/cleanroommc/modularui/widget/Widget.java index c79b17c3b..a29f2c5e2 100644 --- a/src/main/java/com/cleanroommc/modularui/widget/Widget.java +++ b/src/main/java/com/cleanroommc/modularui/widget/Widget.java @@ -5,6 +5,7 @@ import com.cleanroommc.modularui.api.drawable.IDrawable; import com.cleanroommc.modularui.api.layout.IResizeable; import com.cleanroommc.modularui.api.layout.IViewportStack; +import com.cleanroommc.modularui.api.value.ISyncOrValue; import com.cleanroommc.modularui.api.value.IValue; import com.cleanroommc.modularui.api.widget.IDragResizeable; import com.cleanroommc.modularui.api.widget.IGuiAction; @@ -150,12 +151,7 @@ public void initialiseSyncHandler(ModularSyncManager syncManager, boolean late) if (handler == null && this.syncKey != null) { handler = syncManager.getSyncHandler(getPanel().getName(), this.syncKey); } - if (handler != null) checkValidSyncHandler(handler); - if (handler instanceof IValue value1) { - setValue(value1); // this also calls setSyncHandler when a sync handler is passed in - } else { - setSyncHandler(handler); - } + if (handler != null) setSyncOrValue(handler); if (this.syncHandler instanceof ValueSyncHandler valueSyncHandler && valueSyncHandler.getChangeListener() == null) { valueSyncHandler.setChangeListener(this::markTooltipDirty); } @@ -855,6 +851,8 @@ public W syncHandler(String name, int id) { * Used for widgets to set a value handler.
* Will also call {@link #setSyncHandler(SyncHandler)} if it is a SyncHandler */ + @ApiStatus.ScheduledForRemoval(inVersion = "3.2.0") + @Deprecated protected void setValue(IValue value) { this.value = value; if (value instanceof SyncHandler handler) { @@ -865,11 +863,21 @@ protected void setValue(IValue value) { /** * Used for widgets to set a sync handler. */ + @ApiStatus.ScheduledForRemoval(inVersion = "3.2.0") + @Deprecated protected void setSyncHandler(@Nullable SyncHandler syncHandler) { - if (syncHandler != null) checkValidSyncHandler(syncHandler); + if (syncHandler != null) checkValidSyncOrValue(syncHandler); this.syncHandler = syncHandler; } + @MustBeInvokedByOverriders + protected void setSyncOrValue(@NotNull ISyncOrValue syncOrValue) { + if (!syncOrValue.isSyncHandler() && !syncOrValue.isValueHandler()) return; + checkValidSyncOrValue(syncOrValue); + if (syncOrValue instanceof SyncHandler syncHandler1) setSyncHandler(syncHandler1); + if (syncOrValue instanceof IValue value1) setValue(value1); + } + // ------------- // === Other === // ------------- diff --git a/src/main/java/com/cleanroommc/modularui/widgets/AbstractCycleButtonWidget.java b/src/main/java/com/cleanroommc/modularui/widgets/AbstractCycleButtonWidget.java index 6c4ed059f..0196b1ba9 100644 --- a/src/main/java/com/cleanroommc/modularui/widgets/AbstractCycleButtonWidget.java +++ b/src/main/java/com/cleanroommc/modularui/widgets/AbstractCycleButtonWidget.java @@ -7,14 +7,13 @@ import com.cleanroommc.modularui.api.value.IBoolValue; import com.cleanroommc.modularui.api.value.IEnumValue; import com.cleanroommc.modularui.api.value.IIntValue; -import com.cleanroommc.modularui.api.value.IValue; +import com.cleanroommc.modularui.api.value.ISyncOrValue; import com.cleanroommc.modularui.api.widget.Interactable; import com.cleanroommc.modularui.drawable.UITexture; import com.cleanroommc.modularui.screen.RichTooltip; import com.cleanroommc.modularui.theme.WidgetThemeEntry; import com.cleanroommc.modularui.utils.Alignment; import com.cleanroommc.modularui.value.IntValue; -import com.cleanroommc.modularui.value.sync.SyncHandler; import com.cleanroommc.modularui.widget.Widget; import org.jetbrains.annotations.NotNull; @@ -46,23 +45,18 @@ public void onInit() { } @Override - public boolean isValidSyncHandler(SyncHandler syncHandler) { - return syncHandler instanceof IIntValue; + public boolean isValidSyncOrValue(@NotNull ISyncOrValue syncOrValue) { + return syncOrValue.isTypeOrEmpty(IIntValue.class); } @Override - protected void setSyncHandler(@Nullable SyncHandler syncHandler) { - super.setSyncHandler(syncHandler); - if (syncHandler != null) { - this.intValue = castIfTypeElseNull(syncHandler, IIntValue.class); - } - } - - @Override - protected void setValue(IValue value) { - super.setValue(value); - if (value instanceof IIntValue intValue1) { - this.intValue = intValue1; + protected void setSyncOrValue(@NotNull ISyncOrValue syncOrValue) { + super.setSyncOrValue(syncOrValue); + this.intValue = syncOrValue.castNullable(IIntValue.class); + if (syncOrValue instanceof IEnumValue enumValue) { + stateCount(enumValue.getEnumClass().getEnumConstants().length); + } else if (syncOrValue instanceof IBoolValue) { + stateCount(2); } } @@ -195,13 +189,7 @@ public W disableHoverOverlay() { } protected W value(IIntValue value) { - this.intValue = value; - setValue(value); - if (value instanceof IEnumValue enumValue) { - stateCount(enumValue.getEnumClass().getEnumConstants().length); - } else if (value instanceof IBoolValue) { - stateCount(2); - } + setSyncOrValue(ISyncOrValue.orEmpty(value)); return getThis(); } diff --git a/src/main/java/com/cleanroommc/modularui/widgets/ButtonWidget.java b/src/main/java/com/cleanroommc/modularui/widgets/ButtonWidget.java index da734fd89..fdaa89833 100644 --- a/src/main/java/com/cleanroommc/modularui/widgets/ButtonWidget.java +++ b/src/main/java/com/cleanroommc/modularui/widgets/ButtonWidget.java @@ -3,16 +3,15 @@ import com.cleanroommc.modularui.api.ITheme; import com.cleanroommc.modularui.api.IThemeApi; import com.cleanroommc.modularui.api.UpOrDown; +import com.cleanroommc.modularui.api.value.ISyncOrValue; import com.cleanroommc.modularui.api.widget.IGuiAction; import com.cleanroommc.modularui.api.widget.Interactable; import com.cleanroommc.modularui.drawable.GuiTextures; import com.cleanroommc.modularui.theme.WidgetThemeEntry; import com.cleanroommc.modularui.value.sync.InteractionSyncHandler; -import com.cleanroommc.modularui.value.sync.SyncHandler; import com.cleanroommc.modularui.widget.SingleChildWidget; import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; public class ButtonWidget> extends SingleChildWidget implements Interactable { @@ -43,13 +42,19 @@ public static ButtonWidget panelCloseButton() { private InteractionSyncHandler syncHandler; @Override - public boolean isValidSyncHandler(SyncHandler syncHandler) { - return syncHandler instanceof InteractionSyncHandler; + public WidgetThemeEntry getWidgetThemeInternal(ITheme theme) { + return theme.getButtonTheme(); } @Override - public WidgetThemeEntry getWidgetThemeInternal(ITheme theme) { - return theme.getButtonTheme(); + public boolean isValidSyncOrValue(@NotNull ISyncOrValue syncOrValue) { + return syncOrValue.isTypeOrEmpty(InteractionSyncHandler.class); + } + + @Override + protected void setSyncOrValue(@NotNull ISyncOrValue syncOrValue) { + super.setSyncOrValue(syncOrValue); + this.syncHandler = syncOrValue.castNullable(InteractionSyncHandler.class); } public void playClickSound() { @@ -166,16 +171,10 @@ public W onKeyTapped(IGuiAction.KeyPressed keyTapped) { } public W syncHandler(InteractionSyncHandler interactionSyncHandler) { - setSyncHandler(interactionSyncHandler); + setSyncOrValue(ISyncOrValue.orEmpty(interactionSyncHandler)); return getThis(); } - @Override - protected void setSyncHandler(@Nullable SyncHandler syncHandler) { - super.setSyncHandler(syncHandler); - this.syncHandler = castIfTypeElseNull(syncHandler, InteractionSyncHandler.class); - } - public W playClickSound(boolean play) { this.playClickSound = play; return getThis(); diff --git a/src/main/java/com/cleanroommc/modularui/widgets/DynamicSyncedWidget.java b/src/main/java/com/cleanroommc/modularui/widgets/DynamicSyncedWidget.java index c494fd373..8e57dba3f 100644 --- a/src/main/java/com/cleanroommc/modularui/widgets/DynamicSyncedWidget.java +++ b/src/main/java/com/cleanroommc/modularui/widgets/DynamicSyncedWidget.java @@ -1,12 +1,12 @@ package com.cleanroommc.modularui.widgets; +import com.cleanroommc.modularui.api.value.ISyncOrValue; import com.cleanroommc.modularui.api.widget.IWidget; import com.cleanroommc.modularui.value.sync.DynamicSyncHandler; import com.cleanroommc.modularui.value.sync.SyncHandler; import com.cleanroommc.modularui.widget.Widget; import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; import java.util.Collections; import java.util.List; @@ -26,14 +26,15 @@ public class DynamicSyncedWidget> extends Widge private IWidget child; @Override - public boolean isValidSyncHandler(SyncHandler syncHandler) { - return syncHandler instanceof DynamicSyncHandler; + public boolean isValidSyncOrValue(@NotNull ISyncOrValue syncOrValue) { + return syncOrValue.isTypeOrEmpty(DynamicSyncHandler.class); } @Override - protected void setSyncHandler(@Nullable SyncHandler syncHandler) { - super.setSyncHandler(syncHandler); - this.syncHandler = castIfTypeElseNull(syncHandler, DynamicSyncHandler.class, t -> t.attachDynamicWidgetListener(this::updateChild)); + protected void setSyncOrValue(@NotNull ISyncOrValue syncOrValue) { + super.setSyncOrValue(syncOrValue); + this.syncHandler = syncOrValue.castNullable(DynamicSyncHandler.class); + if (this.syncHandler != null) this.syncHandler.attachDynamicWidgetListener(this::updateChild); } @Override @@ -59,7 +60,7 @@ private void updateChild(IWidget widget) { } public W syncHandler(DynamicSyncHandler syncHandler) { - setSyncHandler(syncHandler); + setSyncOrValue(ISyncOrValue.orEmpty(syncHandler)); return getThis(); } diff --git a/src/main/java/com/cleanroommc/modularui/widgets/Expandable.java b/src/main/java/com/cleanroommc/modularui/widgets/Expandable.java index 339189877..c77de8fcd 100644 --- a/src/main/java/com/cleanroommc/modularui/widgets/Expandable.java +++ b/src/main/java/com/cleanroommc/modularui/widgets/Expandable.java @@ -4,12 +4,10 @@ import com.cleanroommc.modularui.animation.MutableObjectAnimator; import com.cleanroommc.modularui.api.drawable.IInterpolation; import com.cleanroommc.modularui.api.layout.IViewport; -import com.cleanroommc.modularui.api.layout.IViewportStack; import com.cleanroommc.modularui.api.widget.IWidget; import com.cleanroommc.modularui.api.widget.Interactable; import com.cleanroommc.modularui.drawable.Stencil; import com.cleanroommc.modularui.screen.viewport.ModularGuiContext; -import com.cleanroommc.modularui.utils.HoveredWidgetList; import com.cleanroommc.modularui.utils.Interpolation; import com.cleanroommc.modularui.widget.EmptyWidget; import com.cleanroommc.modularui.widget.Widget; diff --git a/src/main/java/com/cleanroommc/modularui/widgets/ItemDisplayWidget.java b/src/main/java/com/cleanroommc/modularui/widgets/ItemDisplayWidget.java index 74fd3a49a..053bf9b13 100644 --- a/src/main/java/com/cleanroommc/modularui/widgets/ItemDisplayWidget.java +++ b/src/main/java/com/cleanroommc/modularui/widgets/ItemDisplayWidget.java @@ -1,6 +1,7 @@ package com.cleanroommc.modularui.widgets; import com.cleanroommc.modularui.api.ITheme; +import com.cleanroommc.modularui.api.value.ISyncOrValue; import com.cleanroommc.modularui.api.value.IValue; import com.cleanroommc.modularui.drawable.GuiDraw; import com.cleanroommc.modularui.screen.viewport.ModularGuiContext; @@ -9,13 +10,11 @@ import com.cleanroommc.modularui.utils.Platform; import com.cleanroommc.modularui.value.ObjectValue; import com.cleanroommc.modularui.value.sync.GenericSyncValue; -import com.cleanroommc.modularui.value.sync.SyncHandler; import com.cleanroommc.modularui.widget.Widget; import net.minecraft.item.ItemStack; -import org.jetbrains.annotations.Nullable; -import scala.tools.nsc.doc.model.Class; +import org.jetbrains.annotations.NotNull; /** * An item slot which only purpose is to display an item stack. @@ -32,22 +31,14 @@ public ItemDisplayWidget() { } @Override - public boolean isValidSyncHandler(SyncHandler syncHandler) { - return syncHandler instanceof GenericSyncValue gsv && gsv.isOfType(ItemStack.class); + public boolean isValidSyncOrValue(@NotNull ISyncOrValue syncOrValue) { + return syncOrValue.isValueOfType(ItemStack.class); } @Override - protected void setSyncHandler(@Nullable SyncHandler syncHandler) { - super.setSyncHandler(syncHandler); - if (syncHandler != null) { - this.value = castIfTypeGenericElseNull(syncHandler, ItemStack.class); - } - } - - @Override - protected void setValue(IValue value) { - super.setValue(value); - this.value = (IValue) value; + protected void setSyncOrValue(@NotNull ISyncOrValue syncOrValue) { + super.setSyncOrValue(syncOrValue); + this.value = syncOrValue.castValueNullable(ItemStack.class); } @Override @@ -76,13 +67,12 @@ public void draw(ModularGuiContext context, WidgetThemeEntry widgetTheme) { } public ItemDisplayWidget item(IValue itemSupplier) { - this.value = itemSupplier; - setValue(itemSupplier); + setSyncOrValue(ISyncOrValue.orEmpty(itemSupplier)); return this; } public ItemDisplayWidget item(ItemStack itemStack) { - return item(new ObjectValue<>(itemStack)); + return item(new ObjectValue<>(ItemStack.class, itemStack)); } public ItemDisplayWidget displayAmount(boolean displayAmount) { diff --git a/src/main/java/com/cleanroommc/modularui/widgets/ProgressWidget.java b/src/main/java/com/cleanroommc/modularui/widgets/ProgressWidget.java index a3099fa8d..de79e2ad2 100644 --- a/src/main/java/com/cleanroommc/modularui/widgets/ProgressWidget.java +++ b/src/main/java/com/cleanroommc/modularui/widgets/ProgressWidget.java @@ -2,7 +2,7 @@ import com.cleanroommc.modularui.ModularUIConfig; import com.cleanroommc.modularui.api.value.IDoubleValue; -import com.cleanroommc.modularui.api.value.IValue; +import com.cleanroommc.modularui.api.value.ISyncOrValue; import com.cleanroommc.modularui.drawable.UITexture; import com.cleanroommc.modularui.screen.viewport.ModularGuiContext; import com.cleanroommc.modularui.theme.WidgetTheme; @@ -10,10 +10,9 @@ import com.cleanroommc.modularui.utils.Color; import com.cleanroommc.modularui.utils.MathUtils; import com.cleanroommc.modularui.value.DoubleValue; -import com.cleanroommc.modularui.value.sync.SyncHandler; import com.cleanroommc.modularui.widget.Widget; -import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.NotNull; import java.util.function.DoubleSupplier; @@ -41,24 +40,14 @@ public void onInit() { } @Override - public boolean isValidSyncHandler(SyncHandler syncHandler) { - return syncHandler instanceof IDoubleValue; + public boolean isValidSyncOrValue(@NotNull ISyncOrValue syncOrValue) { + return syncOrValue.isTypeOrEmpty(IDoubleValue.class); } @Override - protected void setSyncHandler(@Nullable SyncHandler syncHandler) { - super.setSyncHandler(syncHandler); - if (syncHandler != null) { - this.doubleValue = castIfTypeElseNull(syncHandler, IDoubleValue.class); - } - } - - @Override - protected void setValue(IValue value) { - super.setValue(value); - if (value instanceof IDoubleValue value1) { - this.doubleValue = value1; - } + protected void setSyncOrValue(@NotNull ISyncOrValue syncOrValue) { + super.setSyncOrValue(syncOrValue); + this.doubleValue = syncOrValue.castNullable(IDoubleValue.class); } @Override @@ -170,8 +159,7 @@ private void drawCircular(float progress, WidgetTheme widgetTheme) { } public ProgressWidget value(IDoubleValue value) { - this.doubleValue = value; - setValue(value); + setSyncOrValue(ISyncOrValue.orEmpty(value)); return this; } diff --git a/src/main/java/com/cleanroommc/modularui/widgets/SliderWidget.java b/src/main/java/com/cleanroommc/modularui/widgets/SliderWidget.java index 53cadf3cf..cc501b0eb 100644 --- a/src/main/java/com/cleanroommc/modularui/widgets/SliderWidget.java +++ b/src/main/java/com/cleanroommc/modularui/widgets/SliderWidget.java @@ -3,7 +3,7 @@ import com.cleanroommc.modularui.api.GuiAxis; import com.cleanroommc.modularui.api.drawable.IDrawable; import com.cleanroommc.modularui.api.value.IDoubleValue; -import com.cleanroommc.modularui.api.value.IValue; +import com.cleanroommc.modularui.api.value.ISyncOrValue; import com.cleanroommc.modularui.api.widget.IGuiAction; import com.cleanroommc.modularui.api.widget.Interactable; import com.cleanroommc.modularui.drawable.GuiTextures; @@ -14,7 +14,6 @@ import com.cleanroommc.modularui.utils.Color; import com.cleanroommc.modularui.utils.MathUtils; import com.cleanroommc.modularui.value.DoubleValue; -import com.cleanroommc.modularui.value.sync.SyncHandler; import com.cleanroommc.modularui.widget.Widget; import com.cleanroommc.modularui.widget.sizer.Area; import com.cleanroommc.modularui.widget.sizer.Unit; @@ -22,7 +21,6 @@ import it.unimi.dsi.fastutil.doubles.DoubleArrayList; import it.unimi.dsi.fastutil.doubles.DoubleList; import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; public class SliderWidget extends Widget implements Interactable { @@ -64,24 +62,14 @@ public void onInit() { } @Override - public boolean isValidSyncHandler(SyncHandler syncHandler) { - return syncHandler instanceof IDoubleValue; + public boolean isValidSyncOrValue(@NotNull ISyncOrValue syncOrValue) { + return syncOrValue.isTypeOrEmpty(IDoubleValue.class); } @Override - protected void setSyncHandler(@Nullable SyncHandler syncHandler) { - super.setSyncHandler(syncHandler); - if (syncHandler != null) { - this.doubleValue = castIfTypeElseNull(syncHandler, IDoubleValue.class); - } - } - - @Override - protected void setValue(IValue value) { - super.setValue(value); - if (value instanceof IDoubleValue doubleValue1) { - this.doubleValue = doubleValue1; - } + protected void setSyncOrValue(@NotNull ISyncOrValue syncOrValue) { + super.setSyncOrValue(syncOrValue); + this.doubleValue = syncOrValue.castNullable(IDoubleValue.class); } @Override @@ -211,8 +199,7 @@ public String toString() { } public SliderWidget value(IDoubleValue value) { - this.doubleValue = value; - setValue(value); + setSyncOrValue(ISyncOrValue.orEmpty(value)); return this; } diff --git a/src/main/java/com/cleanroommc/modularui/widgets/slot/FluidSlot.java b/src/main/java/com/cleanroommc/modularui/widgets/slot/FluidSlot.java index b5ca38e7f..e37509ed1 100644 --- a/src/main/java/com/cleanroommc/modularui/widgets/slot/FluidSlot.java +++ b/src/main/java/com/cleanroommc/modularui/widgets/slot/FluidSlot.java @@ -5,6 +5,7 @@ import com.cleanroommc.modularui.api.UpOrDown; import com.cleanroommc.modularui.api.drawable.IDrawable; import com.cleanroommc.modularui.api.drawable.IKey; +import com.cleanroommc.modularui.api.value.ISyncOrValue; import com.cleanroommc.modularui.api.widget.Interactable; import com.cleanroommc.modularui.drawable.GuiDraw; import com.cleanroommc.modularui.drawable.text.TextRenderer; @@ -23,7 +24,6 @@ import com.cleanroommc.modularui.utils.Platform; import com.cleanroommc.modularui.utils.SIPrefix; import com.cleanroommc.modularui.value.sync.FluidSlotSyncHandler; -import com.cleanroommc.modularui.value.sync.SyncHandler; import com.cleanroommc.modularui.widget.Widget; import net.minecraft.item.ItemStack; @@ -144,14 +144,14 @@ public void onInit() { } @Override - public boolean isValidSyncHandler(SyncHandler syncHandler) { - return syncHandler instanceof FluidSlotSyncHandler; + public boolean isValidSyncOrValue(@NotNull ISyncOrValue syncOrValue) { + return syncOrValue.isTypeOrEmpty(FluidSlotSyncHandler.class); } @Override - protected void setSyncHandler(@Nullable SyncHandler syncHandler) { - super.setSyncHandler(syncHandler); - this.syncHandler = castIfTypeElseNull(syncHandler, FluidSlotSyncHandler.class); + protected void setSyncOrValue(@NotNull ISyncOrValue syncOrValue) { + super.setSyncOrValue(syncOrValue); + this.syncHandler = syncOrValue.castNullable(FluidSlotSyncHandler.class); } @Override @@ -284,7 +284,7 @@ public FluidSlot syncHandler(IFluidTank fluidTank) { } public FluidSlot syncHandler(FluidSlotSyncHandler syncHandler) { - setSyncHandler(syncHandler); + setSyncOrValue(ISyncOrValue.orEmpty(syncHandler)); return this; } diff --git a/src/main/java/com/cleanroommc/modularui/widgets/slot/ItemSlot.java b/src/main/java/com/cleanroommc/modularui/widgets/slot/ItemSlot.java index 9eab1851b..08063d11e 100644 --- a/src/main/java/com/cleanroommc/modularui/widgets/slot/ItemSlot.java +++ b/src/main/java/com/cleanroommc/modularui/widgets/slot/ItemSlot.java @@ -3,6 +3,7 @@ import com.cleanroommc.modularui.ModularUI; import com.cleanroommc.modularui.api.ITheme; import com.cleanroommc.modularui.api.IThemeApi; +import com.cleanroommc.modularui.api.value.ISyncOrValue; import com.cleanroommc.modularui.api.widget.IVanillaSlot; import com.cleanroommc.modularui.api.widget.Interactable; import com.cleanroommc.modularui.core.mixins.early.minecraft.GuiAccessor; @@ -20,7 +21,6 @@ import com.cleanroommc.modularui.utils.Platform; import com.cleanroommc.modularui.utils.item.IItemHandlerModifiable; import com.cleanroommc.modularui.value.sync.ItemSlotSH; -import com.cleanroommc.modularui.value.sync.SyncHandler; import com.cleanroommc.modularui.widget.Widget; import com.cleanroommc.neverenoughanimations.NEAConfig; @@ -68,14 +68,15 @@ public void onInit() { } @Override - public boolean isValidSyncHandler(SyncHandler syncHandler) { - return syncHandler instanceof ItemSlotSH; + public boolean isValidSyncOrValue(@NotNull ISyncOrValue syncOrValue) { + // disallow null + return syncOrValue instanceof ItemSlotSH; } @Override - protected void setSyncHandler(@Nullable SyncHandler syncHandler) { - super.setSyncHandler(syncHandler); - this.syncHandler = castIfTypeElseNull(syncHandler, ItemSlotSH.class); + protected void setSyncOrValue(@NotNull ISyncOrValue syncOrValue) { + super.setSyncOrValue(syncOrValue); + this.syncHandler = syncOrValue.castOrThrow(ItemSlotSH.class); } @Override @@ -213,7 +214,7 @@ public ItemSlot slot(IItemHandlerModifiable itemHandler, int index) { } public ItemSlot syncHandler(ItemSlotSH syncHandler) { - setSyncHandler(syncHandler); + setSyncOrValue(ISyncOrValue.orEmpty(syncHandler)); return this; } diff --git a/src/main/java/com/cleanroommc/modularui/widgets/slot/ModularSlot.java b/src/main/java/com/cleanroommc/modularui/widgets/slot/ModularSlot.java index 0257d0dc3..2099f39af 100644 --- a/src/main/java/com/cleanroommc/modularui/widgets/slot/ModularSlot.java +++ b/src/main/java/com/cleanroommc/modularui/widgets/slot/ModularSlot.java @@ -234,4 +234,12 @@ public ModularSlot singletonSlotGroup(int shiftClickPriority) { public ModularSlot singletonSlotGroup() { return singletonSlotGroup(SlotGroup.STORAGE_SLOT_PRIO); } + + public static boolean isPlayerSlot(Slot slot) { + return slot.inventory instanceof InventoryPlayer; + } + + public static boolean isPlayerSlot(SlotItemHandler slot) { + return slot.getItemHandler() instanceof PlayerInvWrapper || slot.getItemHandler() instanceof PlayerMainInvWrapper; + } } diff --git a/src/main/java/com/cleanroommc/modularui/widgets/slot/PhantomItemSlot.java b/src/main/java/com/cleanroommc/modularui/widgets/slot/PhantomItemSlot.java index 2c294b06e..3fdf008d7 100644 --- a/src/main/java/com/cleanroommc/modularui/widgets/slot/PhantomItemSlot.java +++ b/src/main/java/com/cleanroommc/modularui/widgets/slot/PhantomItemSlot.java @@ -1,11 +1,11 @@ package com.cleanroommc.modularui.widgets.slot; import com.cleanroommc.modularui.api.UpOrDown; +import com.cleanroommc.modularui.api.value.ISyncOrValue; import com.cleanroommc.modularui.integration.recipeviewer.RecipeViewerGhostIngredientSlot; import com.cleanroommc.modularui.utils.MouseData; import com.cleanroommc.modularui.value.sync.ItemSlotSH; import com.cleanroommc.modularui.value.sync.PhantomItemSlotSH; -import com.cleanroommc.modularui.value.sync.SyncHandler; import net.minecraft.item.ItemStack; @@ -22,14 +22,14 @@ public void onInit() { } @Override - public boolean isValidSyncHandler(SyncHandler syncHandler) { - return syncHandler instanceof PhantomItemSlotSH; + public boolean isValidSyncOrValue(@NotNull ISyncOrValue syncOrValue) { + return syncOrValue instanceof PhantomItemSlotSH; } @Override - protected void setSyncHandler(@Nullable SyncHandler syncHandler) { - super.setSyncHandler(syncHandler); - this.syncHandler = castIfTypeElseNull(syncHandler, PhantomItemSlotSH.class); + protected void setSyncOrValue(@NotNull ISyncOrValue syncOrValue) { + super.setSyncOrValue(syncOrValue); + this.syncHandler = syncOrValue.castOrThrow(PhantomItemSlotSH.class); } @Override @@ -86,7 +86,7 @@ public PhantomItemSlot slot(ModularSlot slot) { @Override public PhantomItemSlot syncHandler(ItemSlotSH syncHandler) { - setSyncHandler(syncHandler); + setSyncOrValue(ISyncOrValue.orEmpty(syncHandler)); return this; } diff --git a/src/main/java/com/cleanroommc/modularui/widgets/textfield/TextFieldWidget.java b/src/main/java/com/cleanroommc/modularui/widgets/textfield/TextFieldWidget.java index eea13d4ec..3073873ea 100644 --- a/src/main/java/com/cleanroommc/modularui/widgets/textfield/TextFieldWidget.java +++ b/src/main/java/com/cleanroommc/modularui/widgets/textfield/TextFieldWidget.java @@ -6,18 +6,16 @@ import com.cleanroommc.modularui.api.drawable.IKey; import com.cleanroommc.modularui.api.drawable.ITextLine; import com.cleanroommc.modularui.api.value.IStringValue; -import com.cleanroommc.modularui.api.value.IValue; +import com.cleanroommc.modularui.api.value.ISyncOrValue; import com.cleanroommc.modularui.api.widget.Interactable; import com.cleanroommc.modularui.screen.RichTooltip; import com.cleanroommc.modularui.screen.viewport.ModularGuiContext; import com.cleanroommc.modularui.utils.MathUtils; import com.cleanroommc.modularui.utils.ParseResult; import com.cleanroommc.modularui.value.StringValue; -import com.cleanroommc.modularui.value.sync.SyncHandler; import com.cleanroommc.modularui.value.sync.ValueSyncHandler; import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; import java.text.ParsePosition; import java.util.function.Consumer; @@ -70,14 +68,15 @@ public void onInit() { } @Override - public boolean isValidSyncHandler(SyncHandler syncHandler) { - return syncHandler instanceof IStringValue; + public boolean isValidSyncOrValue(@NotNull ISyncOrValue syncOrValue) { + return syncOrValue.isTypeOrEmpty(IStringValue.class); } @Override - protected void setSyncHandler(@Nullable SyncHandler syncHandler) { - super.setSyncHandler(syncHandler); - if (syncHandler instanceof ValueSyncHandler valueSyncHandler) { + protected void setSyncOrValue(@NotNull ISyncOrValue syncOrValue) { + super.setSyncOrValue(syncOrValue); + this.stringValue = syncOrValue.castNullable(IStringValue.class); + if (syncOrValue instanceof ValueSyncHandler valueSyncHandler) { valueSyncHandler.setChangeListener(() -> { markTooltipDirty(); setText(this.stringValue.getValue().toString()); @@ -85,14 +84,6 @@ protected void setSyncHandler(@Nullable SyncHandler syncHandler) { } } - @Override - protected void setValue(IValue value) { - super.setValue(value); - if (value instanceof IStringValue stringValue1) { - this.stringValue = stringValue1; - } - } - @Override public void onUpdate() { super.onUpdate(); @@ -235,8 +226,7 @@ public TextFieldWidget setFormatAsInteger(boolean formatAsInteger) { } public TextFieldWidget value(IStringValue stringValue) { - this.stringValue = stringValue; - setValue(stringValue); + setSyncOrValue(ISyncOrValue.orEmpty(stringValue)); return this; } From fc64fa3c721f73067c1334c519440cd5a5067003 Mon Sep 17 00:00:00 2001 From: brachy84 Date: Sat, 6 Dec 2025 13:18:42 +0100 Subject: [PATCH 08/50] fix #182 (cherry picked from commit f5a532e5601143b8bbf16eeab826ee974a09cd0d) --- .../cleanroommc/modularui/CommonProxy.java | 14 +++++++++++++ .../modularui/screen/ClientScreenHandler.java | 2 +- .../modularui/screen/ModularContainer.java | 21 ++++++++++++++----- .../modularui/screen/ModularPanel.java | 7 +------ .../modularui/screen/PanelManager.java | 17 ++++++++++++--- .../cleanroommc/modularui/test/TestTile.java | 6 +++--- .../cleanroommc/modularui/test/TestTile2.java | 4 ++-- .../value/sync/ModularSyncManager.java | 3 ++- 8 files changed, 53 insertions(+), 21 deletions(-) diff --git a/src/main/java/com/cleanroommc/modularui/CommonProxy.java b/src/main/java/com/cleanroommc/modularui/CommonProxy.java index a99d91c2e..6541d8656 100644 --- a/src/main/java/com/cleanroommc/modularui/CommonProxy.java +++ b/src/main/java/com/cleanroommc/modularui/CommonProxy.java @@ -62,6 +62,20 @@ public Timer getTimer60Fps() { throw new UnsupportedOperationException(); } + @SubscribeEvent + public void onOpenContainer(PlayerContainerEvent.Open event) { + if (event.getContainer() instanceof ModularContainer container) { + container.onModularContainerOpened(); + } + } + + @SubscribeEvent + public void onCloseContainer(PlayerContainerEvent.Close event) { + if (event.getContainer() instanceof ModularContainer container) { + container.onModularContainerClosed(); + } + } + @SubscribeEvent public void onTick(TickEvent.PlayerTickEvent event) { if (event.player.openContainer instanceof ModularContainer container) { diff --git a/src/main/java/com/cleanroommc/modularui/screen/ClientScreenHandler.java b/src/main/java/com/cleanroommc/modularui/screen/ClientScreenHandler.java index 013b86074..0cc4e352f 100644 --- a/src/main/java/com/cleanroommc/modularui/screen/ClientScreenHandler.java +++ b/src/main/java/com/cleanroommc/modularui/screen/ClientScreenHandler.java @@ -241,7 +241,7 @@ private static void invalidateCurrentScreen() { if (lastMui != null) { ((GuiScreenAccessor) lastMui.getGuiScreen()).setEventButton(-1); ((GuiScreenAccessor) lastMui.getGuiScreen()).setLastMouseEvent(-1); - lastMui.getScreen().getPanelManager().closeAll(); + lastMui.getScreen().getPanelManager().closeScreen(); lastMui = null; } currentScreen = null; diff --git a/src/main/java/com/cleanroommc/modularui/screen/ModularContainer.java b/src/main/java/com/cleanroommc/modularui/screen/ModularContainer.java index b70c33a5b..d3fd20838 100644 --- a/src/main/java/com/cleanroommc/modularui/screen/ModularContainer.java +++ b/src/main/java/com/cleanroommc/modularui/screen/ModularContainer.java @@ -13,6 +13,7 @@ import com.cleanroommc.modularui.widgets.slot.ModularSlot; import com.cleanroommc.modularui.widgets.slot.SlotGroup; +import net.minecraft.client.gui.inventory.GuiContainer; import net.minecraft.entity.player.EntityPlayer; import net.minecraft.entity.player.InventoryPlayer; import net.minecraft.inventory.Container; @@ -98,11 +99,21 @@ public ContainerAccessor acc() { } @MustBeInvokedByOverriders - @Override - public void onContainerClosed(@NotNull EntityPlayer playerIn) { - super.onContainerClosed(playerIn); + public void onModularContainerOpened() { + if (this.syncManager != null) { + this.syncManager.onOpen(); + } + } + + /** + * Called when this container closes. This is different to {@link Container#onContainerClosed(EntityPlayer)}, since that one is also + * called from {@link GuiContainer#onGuiClosed()}, which means it is called even when the container may still exist. + * This happens when a temporary client screen takes over (like JEI,NEI,etc.). This is only called when the container actually closes. + */ + @MustBeInvokedByOverriders + public void onModularContainerClosed() { if (this.syncManager != null) { - this.syncManager.onClose(); + this.syncManager.dispose(); } } @@ -116,7 +127,7 @@ public void detectAndSendChanges() { this.init = false; } - @ApiStatus.Internal + @MustBeInvokedByOverriders public void onUpdate() { // detectAndSendChanges is potentially called multiple times per tick, while this method is called exactly once per tick if (this.syncManager != null) { diff --git a/src/main/java/com/cleanroommc/modularui/screen/ModularPanel.java b/src/main/java/com/cleanroommc/modularui/screen/ModularPanel.java index 724e5ae98..8e9997c6f 100644 --- a/src/main/java/com/cleanroommc/modularui/screen/ModularPanel.java +++ b/src/main/java/com/cleanroommc/modularui/screen/ModularPanel.java @@ -245,13 +245,8 @@ public void onOpen(ModularScreen screen) { this.state = State.OPEN; } - boolean reopen(boolean strict) { - if (this.state != State.CLOSED) { - if (strict) throw new IllegalStateException(); - return false; - } + void reopen() { this.state = State.OPEN; - return true; } @MustBeInvokedByOverriders diff --git a/src/main/java/com/cleanroommc/modularui/screen/PanelManager.java b/src/main/java/com/cleanroommc/modularui/screen/PanelManager.java index a254b15f2..21390b66d 100644 --- a/src/main/java/com/cleanroommc/modularui/screen/PanelManager.java +++ b/src/main/java/com/cleanroommc/modularui/screen/PanelManager.java @@ -60,7 +60,7 @@ boolean tryInit() { throw new IllegalStateException("Tried to reopen closed screen, but all panels are disposed!"); } // set all stored panels to be open - this.panels.forEach(p -> p.reopen(true)); + this.panels.forEach(ModularPanel::reopen); this.disposal.removeIf(this.panels::contains); setState(State.REOPENED); yield true; @@ -226,8 +226,17 @@ public boolean closeAll() { return false; } + void closeScreen() { + // only close the screen without closing the panels + // this is useful when we expect the screen to reopen at some point and the sync managers are still available + if (this.state.isOpen) { + setState(State.CLOSED); + this.screen.onClose(); + } + } + private void finalizePanel(ModularPanel panel) { - panel.onClose(); + if (panel.isOpen()) panel.onClose(); if (!this.disposal.contains(panel)) { if (this.disposal.size() == DISPOSAL_CAPACITY) { this.disposal.removeFirst().dispose(); @@ -258,6 +267,8 @@ public void dispose() { setState(State.WAIT_DISPOSAL); return; } + // make sure every panel gets closed before disposing + this.panels.forEach(this::finalizePanel); setState(State.CLOSED); this.disposal.forEach(ModularPanel::dispose); this.disposal.clear(); @@ -449,7 +460,7 @@ public enum State { */ REOPENED(true), /** - * Screen is closed after it was open. + * Screen is closed after it was open. Panels may still be considered open in some cases. */ CLOSED(false), /** diff --git a/src/main/java/com/cleanroommc/modularui/test/TestTile.java b/src/main/java/com/cleanroommc/modularui/test/TestTile.java index d9ba47c71..4bb963d33 100644 --- a/src/main/java/com/cleanroommc/modularui/test/TestTile.java +++ b/src/main/java/com/cleanroommc/modularui/test/TestTile.java @@ -175,7 +175,7 @@ public ModularPanel buildUI(PosGuiData guiData, PanelSyncManager syncManager, UI Rectangle colorPickerBackground = new Rectangle().setColor(Color.RED.main); ModularPanel panel = new ModularPanel("test_tile"); - IPanelHandler panelSyncHandler = syncManager.panel("other_panel", this::openSecondWindow, true); + IPanelHandler panelSyncHandler = syncManager.syncedPanel("other_panel", true, this::openSecondWindow); IPanelHandler colorPicker = IPanelHandler.simple(panel, (mainPanel, player) -> new ColorPickerDialog(colorPickerBackground::setColor, colorPickerBackground.getColor(), true) .setDraggable(true) .relative(panel) @@ -526,8 +526,8 @@ public ModularPanel openSecondWindow(PanelSyncManager syncManager, IPanelHandler syncManager.registerSlotGroup(slotGroup); AtomicInteger number = new AtomicInteger(0); syncManager.syncValue("int_value", new IntSyncValue(number::get, number::set)); - IPanelHandler panelSyncHandler = syncManager.panel("other_panel_2", (syncManager1, syncHandler1) -> - openThirdWindow(syncManager1, syncHandler1, number), true); + IPanelHandler panelSyncHandler = syncManager.syncedPanel("other_panel_2", true, (syncManager1, syncHandler1) -> + openThirdWindow(syncManager1, syncHandler1, number)); panel.child(ButtonWidget.panelCloseButton()) .child(new ButtonWidget<>() .size(10).top(14).right(4) diff --git a/src/main/java/com/cleanroommc/modularui/test/TestTile2.java b/src/main/java/com/cleanroommc/modularui/test/TestTile2.java index 9da7e10a8..9947d6781 100644 --- a/src/main/java/com/cleanroommc/modularui/test/TestTile2.java +++ b/src/main/java/com/cleanroommc/modularui/test/TestTile2.java @@ -52,7 +52,7 @@ public ModularPanel buildUI(PosGuiData data, PanelSyncManager syncManager, UISet .slot(new ModularSlot(this.itemHandler, i))); } ModularPanel panel = ModularPanel.defaultPanel("test_tile_2", 176, 13 * 18 + 14 + 10 + 20); - IPanelHandler otherPanel = syncManager.panel("2nd panel", (syncManager1, syncHandler) -> { + IPanelHandler otherPanel = syncManager.syncedPanel("2nd panel", true, (syncManager1, syncHandler) -> { ModularPanel panel1 = new Dialog<>("Option Selection").setDisablePanelsBelow(false).setDraggable(true).size(4 * 18 + 8, 4 * 18 + 8); return panel1 .child(SlotGroupWidget.builder() @@ -64,7 +64,7 @@ public ModularPanel buildUI(PosGuiData data, PanelSyncManager syncManager, UISet .build() .pos(4, 4) ); - }, true); + }); return panel .bindPlayerInventory() .child(sw) diff --git a/src/main/java/com/cleanroommc/modularui/value/sync/ModularSyncManager.java b/src/main/java/com/cleanroommc/modularui/value/sync/ModularSyncManager.java index 15f37894b..be455e228 100644 --- a/src/main/java/com/cleanroommc/modularui/value/sync/ModularSyncManager.java +++ b/src/main/java/com/cleanroommc/modularui/value/sync/ModularSyncManager.java @@ -59,8 +59,9 @@ public void detectAndSendChanges(boolean init) { this.panelSyncManagerMap.values().forEach(psm -> psm.detectAndSendChanges(init)); } - public void onClose() { + public void dispose() { this.panelSyncManagerMap.values().forEach(PanelSyncManager::onClose); + this.panelSyncManagerMap.clear(); } public void onOpen() { From 46b08402a4becc84a450daff58c2192b24d4c476 Mon Sep 17 00:00:00 2001 From: brachy84 Date: Sat, 6 Dec 2025 13:19:19 +0100 Subject: [PATCH 09/50] rename WidgetTree.stream() to WidgetTree.flatStream() (cherry picked from commit acca0352e312a406206d0348e5ee3d3ff91b75b1) --- .../java/com/cleanroommc/modularui/widget/WidgetTree.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/cleanroommc/modularui/widget/WidgetTree.java b/src/main/java/com/cleanroommc/modularui/widget/WidgetTree.java index 4b15bf44e..6e2a3af08 100644 --- a/src/main/java/com/cleanroommc/modularui/widget/WidgetTree.java +++ b/src/main/java/com/cleanroommc/modularui/widget/WidgetTree.java @@ -193,7 +193,7 @@ public static boolean foreachChildReverse(IWidget parent, Predicate con } /** - * Creates a stream of the whole sub widget tree. + * Creates a flat stream of the whole sub widget tree. *

* {@link Stream#forEach(Consumer)} on this has slightly worse performance than {@link #foreachChildBFS(IWidget, Predicate, boolean)} on * small widget trees and has similar performance on large widget trees. The performance is significantly better than @@ -202,7 +202,7 @@ public static boolean foreachChildReverse(IWidget parent, Predicate con * @param parent starting point. * @return stream of the sub widget tree */ - public static Stream stream(IWidget parent) { + public static Stream flatStream(IWidget parent) { if (!parent.hasChildren()) return Stream.of(parent); return Streams.stream(iteratorBFS(parent)); } From 47a00f650f58a844178bdf0a077886a00b086683 Mon Sep 17 00:00:00 2001 From: brachy84 Date: Sat, 6 Dec 2025 13:33:58 +0100 Subject: [PATCH 10/50] fix #179 (cherry picked from commit 78a9e2d253b836c201406ef0c4e6e1ae0bb1c7e3) --- .../com/cleanroommc/modularui/value/sync/PanelSyncManager.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/com/cleanroommc/modularui/value/sync/PanelSyncManager.java b/src/main/java/com/cleanroommc/modularui/value/sync/PanelSyncManager.java index 636645767..3a6765706 100644 --- a/src/main/java/com/cleanroommc/modularui/value/sync/PanelSyncManager.java +++ b/src/main/java/com/cleanroommc/modularui/value/sync/PanelSyncManager.java @@ -99,6 +99,7 @@ public void onOpen() { @ApiStatus.Internal public void onClose() { this.closeListener.forEach(listener -> listener.accept(getPlayer())); + this.syncHandlers.values().forEach(SyncHandler::dispose); // Previously panel sync handlers were removed from the main psm, however this problematic if the screen will be reopened at some // point. We can just not remove the sync handlers since mui has proper checks for re-registering panels. } From 8d54505dd9be6aecbcf99a35f3433677ef58c0ec Mon Sep 17 00:00:00 2001 From: brachy84 Date: Sat, 6 Dec 2025 15:24:30 +0100 Subject: [PATCH 11/50] fix cycle button widget crash when no tooltip (cherry picked from commit 039f8490245ca58c915740f956b61148e4590f3b) --- .../modularui/widgets/AbstractCycleButtonWidget.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/cleanroommc/modularui/widgets/AbstractCycleButtonWidget.java b/src/main/java/com/cleanroommc/modularui/widgets/AbstractCycleButtonWidget.java index 0196b1ba9..717e613d0 100644 --- a/src/main/java/com/cleanroommc/modularui/widgets/AbstractCycleButtonWidget.java +++ b/src/main/java/com/cleanroommc/modularui/widgets/AbstractCycleButtonWidget.java @@ -161,7 +161,10 @@ public void markTooltipDirty() { public @Nullable RichTooltip getTooltip() { RichTooltip tooltip = super.getTooltip(); if (tooltip == null || tooltip.isEmpty()) { - return this.stateTooltip.get(getState()); + int state = getState(); + if (!this.stateTooltip.isEmpty() && this.stateTooltip.size() - 1 <= state) { + return this.stateTooltip.get(getState()); + } } return tooltip; } From 3ac3f4644f0837e5effea757af5478b41749ff54 Mon Sep 17 00:00:00 2001 From: brachy84 Date: Sat, 6 Dec 2025 16:20:38 +0100 Subject: [PATCH 12/50] implicit state count of cycle buttons (cherry picked from commit d4e989e0e5ab8912a1eef17be082e21957c9060a) --- .../widgets/AbstractCycleButtonWidget.java | 137 ++++++++++++------ 1 file changed, 95 insertions(+), 42 deletions(-) diff --git a/src/main/java/com/cleanroommc/modularui/widgets/AbstractCycleButtonWidget.java b/src/main/java/com/cleanroommc/modularui/widgets/AbstractCycleButtonWidget.java index 717e613d0..d711e72d3 100644 --- a/src/main/java/com/cleanroommc/modularui/widgets/AbstractCycleButtonWidget.java +++ b/src/main/java/com/cleanroommc/modularui/widgets/AbstractCycleButtonWidget.java @@ -1,5 +1,6 @@ package com.cleanroommc.modularui.widgets; +import com.cleanroommc.modularui.ModularUI; import com.cleanroommc.modularui.api.ITheme; import com.cleanroommc.modularui.api.drawable.IDrawable; import com.cleanroommc.modularui.api.drawable.IKey; @@ -19,21 +20,23 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import java.util.ArrayList; import java.util.Arrays; -import java.util.List; import java.util.function.Consumer; public class AbstractCycleButtonWidget> extends Widget implements Interactable { + private static final RichTooltip[] EMPTY_TOOLTIP = new RichTooltip[0]; + private int stateCount = 1; + private boolean explicitStateCount = false; + private boolean hasCount = false; private IIntValue intValue; private int lastValue = -1; protected IDrawable[] background = null; protected IDrawable[] hoverBackground = null; protected IDrawable[] overlay = null; protected IDrawable[] hoverOverlay = null; - private final List stateTooltip = new ArrayList<>(); + protected RichTooltip[] tooltip = EMPTY_TOOLTIP; private boolean playClickSound = true; private Runnable clickSound; @@ -49,14 +52,53 @@ public boolean isValidSyncOrValue(@NotNull ISyncOrValue syncOrValue) { return syncOrValue.isTypeOrEmpty(IIntValue.class); } + protected void updateStateCount(int count, boolean explicit) { + if (count < 1) return; + if (explicit) { + setStateCount(count); + this.explicitStateCount = true; + } else if (!this.explicitStateCount && count > this.stateCount) { + setStateCount(count); + } + } + + private void setStateCount(int stateCount) { + this.hasCount = true; + if (this.stateCount == stateCount) return; + this.stateCount = stateCount; + + int currentSize = this.tooltip.length; + if (stateCount > currentSize) { + this.tooltip = Arrays.copyOf(this.tooltip, stateCount); + for (; currentSize < stateCount; currentSize++) { + this.tooltip[currentSize] = new RichTooltip().parent(this); + } + } else if (stateCount < currentSize) { + for (int i = stateCount; i < currentSize; i++) { + this.tooltip[i].reset(); + } + } + + this.background = checkArray(this.background, stateCount); + this.overlay = checkArray(this.overlay, stateCount); + this.hoverBackground = checkArray(this.hoverBackground, stateCount); + this.hoverOverlay = checkArray(this.hoverOverlay, stateCount); + } + + protected void expectCount() { + if (!this.hasCount) { + ModularUI.LOGGER.error("State count for widget {} is required, but has not been set yet!", this); + } + } + @Override protected void setSyncOrValue(@NotNull ISyncOrValue syncOrValue) { super.setSyncOrValue(syncOrValue); this.intValue = syncOrValue.castNullable(IIntValue.class); if (syncOrValue instanceof IEnumValue enumValue) { - stateCount(enumValue.getEnumClass().getEnumConstants().length); + updateStateCount(enumValue.getEnumClass().getEnumConstants().length, true); } else if (syncOrValue instanceof IBoolValue) { - stateCount(2); + updateStateCount(2, true); } } @@ -145,13 +187,13 @@ public IDrawable getCurrentOverlay(ITheme theme, WidgetThemeEntry widgetTheme @Override public boolean hasTooltip() { int state = getState(); - return super.hasTooltip() || (this.stateTooltip.size() > state && !this.stateTooltip.get(state).isEmpty()); + return super.hasTooltip() || (this.tooltip.length > state && !this.tooltip[state].isEmpty()); } @Override public void markTooltipDirty() { super.markTooltipDirty(); - for (RichTooltip tooltip : this.stateTooltip) { + for (RichTooltip tooltip : this.tooltip) { tooltip.markDirty(); } getState(); @@ -162,8 +204,8 @@ public void markTooltipDirty() { RichTooltip tooltip = super.getTooltip(); if (tooltip == null || tooltip.isEmpty()) { int state = getState(); - if (!this.stateTooltip.isEmpty() && this.stateTooltip.size() - 1 <= state) { - return this.stateTooltip.get(getState()); + if (this.tooltip.length > 0 && this.tooltip.length > state) { + return this.tooltip[state]; } } return tooltip; @@ -171,6 +213,7 @@ public void markTooltipDirty() { @Override public W disableHoverBackground() { + expectCount(); if (this.hoverBackground != null) { Arrays.fill(this.hoverBackground, IDrawable.NONE); } @@ -182,6 +225,7 @@ public W disableHoverBackground() { @Override public W disableHoverOverlay() { + expectCount(); if (this.hoverOverlay != null) { Arrays.fill(this.hoverOverlay, IDrawable.NONE); } @@ -204,6 +248,7 @@ protected W value(IIntValue value) { * @return this */ public W stateBackground(UITexture texture) { + expectCount(); splitTexture(texture, this.background); return getThis(); } @@ -216,6 +261,7 @@ public W stateBackground(UITexture texture) { * @return this */ public W stateOverlay(UITexture texture) { + expectCount(); splitTexture(texture, this.overlay); return getThis(); } @@ -228,6 +274,7 @@ public W stateOverlay(UITexture texture) { * @return this */ public W stateHoverBackground(UITexture texture) { + expectCount(); splitTexture(texture, this.hoverBackground); return getThis(); } @@ -240,6 +287,7 @@ public W stateHoverBackground(UITexture texture) { * @return this */ public W stateHoverOverlay(UITexture texture) { + expectCount(); splitTexture(texture, this.hoverOverlay); return getThis(); } @@ -248,10 +296,8 @@ public W stateHoverOverlay(UITexture texture) { * Adds a line to the tooltip */ protected W addTooltip(int state, IDrawable tooltip) { - if (state >= this.stateTooltip.size() || state < 0) { - throw new IndexOutOfBoundsException(); - } - this.stateTooltip.get(state).addLine(tooltip); + updateStateCount(state + 1, false); + this.tooltip[state].addLine(tooltip); return getThis(); } @@ -270,7 +316,8 @@ protected W addTooltip(int state, String tooltip) { */ @Override public W addTooltipElement(String s) { - for (RichTooltip tooltip : this.stateTooltip) { + expectCount(); + for (RichTooltip tooltip : this.tooltip) { tooltip.add(s); } return getThis(); @@ -284,7 +331,8 @@ public W addTooltipElement(String s) { */ @Override public W addTooltipDrawableLines(Iterable lines) { - for (RichTooltip tooltip : this.stateTooltip) { + expectCount(); + for (RichTooltip tooltip : this.tooltip) { tooltip.addDrawableLines(lines); } return getThis(); @@ -298,7 +346,8 @@ public W addTooltipDrawableLines(Iterable lines) { */ @Override public W addTooltipElement(IDrawable drawable) { - for (RichTooltip tooltip : this.stateTooltip) { + expectCount(); + for (RichTooltip tooltip : this.tooltip) { tooltip.add(drawable); } return getThis(); @@ -312,7 +361,8 @@ public W addTooltipElement(IDrawable drawable) { */ @Override public W addTooltipLine(ITextLine line) { - for (RichTooltip tooltip : this.stateTooltip) { + expectCount(); + for (RichTooltip tooltip : this.tooltip) { tooltip.addLine(line); } return getThis(); @@ -326,7 +376,8 @@ public W addTooltipLine(ITextLine line) { */ @Override public W addTooltipLine(IDrawable drawable) { - for (RichTooltip tooltip : this.stateTooltip) { + expectCount(); + for (RichTooltip tooltip : this.tooltip) { tooltip.addLine(drawable); } return getThis(); @@ -340,7 +391,8 @@ public W addTooltipLine(IDrawable drawable) { */ @Override public W addTooltipStringLines(Iterable lines) { - for (RichTooltip tooltip : this.stateTooltip) { + expectCount(); + for (RichTooltip tooltip : this.tooltip) { tooltip.addStringLines(lines); } return getThis(); @@ -354,7 +406,8 @@ public W addTooltipStringLines(Iterable lines) { */ @Override public W tooltipStatic(Consumer tooltipConsumer) { - for (RichTooltip tooltip : this.stateTooltip) { + expectCount(); + for (RichTooltip tooltip : this.tooltip) { tooltipConsumer.accept(tooltip); } return getThis(); @@ -368,7 +421,8 @@ public W tooltipStatic(Consumer tooltipConsumer) { */ @Override public W tooltipDynamic(Consumer tooltipBuilder) { - for (RichTooltip tooltip : this.stateTooltip) { + expectCount(); + for (RichTooltip tooltip : this.tooltip) { tooltip.tooltipBuilder(tooltipBuilder); } return getThis(); @@ -382,7 +436,8 @@ public W tooltipDynamic(Consumer tooltipBuilder) { */ @Override public W tooltipAlignment(Alignment alignment) { - for (RichTooltip tooltip : this.stateTooltip) { + expectCount(); + for (RichTooltip tooltip : this.tooltip) { tooltip.alignment(alignment); } return getThis(); @@ -396,7 +451,8 @@ public W tooltipAlignment(Alignment alignment) { */ @Override public W tooltipPos(RichTooltip.Pos pos) { - for (RichTooltip tooltip : this.stateTooltip) { + expectCount(); + for (RichTooltip tooltip : this.tooltip) { tooltip.pos(pos); } return getThis(); @@ -411,7 +467,8 @@ public W tooltipPos(RichTooltip.Pos pos) { */ @Override public W tooltipPos(int x, int y) { - for (RichTooltip tooltip : this.stateTooltip) { + expectCount(); + for (RichTooltip tooltip : this.tooltip) { tooltip.pos(x, y); } return getThis(); @@ -425,7 +482,8 @@ public W tooltipPos(int x, int y) { */ @Override public W tooltipScale(float scale) { - for (RichTooltip tooltip : this.stateTooltip) { + expectCount(); + for (RichTooltip tooltip : this.tooltip) { tooltip.scale(scale); } return getThis(); @@ -439,7 +497,8 @@ public W tooltipScale(float scale) { */ @Override public W tooltipTextColor(int textColor) { - for (RichTooltip tooltip : this.stateTooltip) { + expectCount(); + for (RichTooltip tooltip : this.tooltip) { tooltip.textColor(textColor); } return getThis(); @@ -453,7 +512,8 @@ public W tooltipTextColor(int textColor) { */ @Override public W tooltipTextShadow(boolean textShadow) { - for (RichTooltip tooltip : this.stateTooltip) { + expectCount(); + for (RichTooltip tooltip : this.tooltip) { tooltip.textShadow(textShadow); } return getThis(); @@ -467,25 +527,15 @@ public W tooltipTextShadow(boolean textShadow) { */ @Override public W tooltipShowUpTimer(int showUpTimer) { - for (RichTooltip tooltip : this.stateTooltip) { + expectCount(); + for (RichTooltip tooltip : this.tooltip) { tooltip.showUpTimer(showUpTimer); } return getThis(); } protected W stateCount(int stateCount) { - this.stateCount = stateCount; - // adjust tooltip buffer size - while (this.stateTooltip.size() < this.stateCount) { - this.stateTooltip.add(new RichTooltip().parent(this)); - } - while (this.stateTooltip.size() > this.stateCount) { - this.stateTooltip.remove(this.stateTooltip.size() - 1); - } - this.background = checkArray(this.background, stateCount); - this.overlay = checkArray(this.overlay, stateCount); - this.hoverBackground = checkArray(this.hoverBackground, stateCount); - this.hoverOverlay = checkArray(this.hoverOverlay, stateCount); + updateStateCount(stateCount, true); return getThis(); } @@ -500,6 +550,7 @@ protected IDrawable[] addToArray(IDrawable[] array, IDrawable[] drawable, int in protected IDrawable[] addToArray(IDrawable[] array, IDrawable drawable, int index) { if (index < 0) throw new IndexOutOfBoundsException(); + updateStateCount(index + 1, false); if (array == null || index >= array.length) { IDrawable[] copy = new IDrawable[(int) (Math.ceil((index + 1) / 4.0) * 4)]; if (array != null) { @@ -519,12 +570,14 @@ protected static void splitTexture(UITexture texture, IDrawable[] dest) { } protected W tooltip(int index, Consumer builder) { - builder.accept(this.stateTooltip.get(index)); + updateStateCount(index + 1, false); + builder.accept(this.tooltip[index]); return getThis(); } protected W tooltipBuilder(int index, Consumer builder) { - this.stateTooltip.get(index).tooltipBuilder(builder); + updateStateCount(index + 1, false); + this.tooltip[index].tooltipBuilder(builder); return getThis(); } From 5f19c18447366ea15255cb9d58739ba10b94defd Mon Sep 17 00:00:00 2001 From: brachy84 Date: Sat, 6 Dec 2025 16:21:02 +0100 Subject: [PATCH 13/50] sync handler hyper visor (cherry picked from commit 924159a904a06ef2df0db3f67bd50b479594a6ef) --- .../modularui/factory/GuiManager.java | 11 +- .../modularui/screen/ModularContainer.java | 7 +- .../modularui/screen/RichTooltip.java | 13 ++ .../cleanroommc/modularui/test/TestTile.java | 10 +- .../modularui/value/sync/ISyncRegistrar.java | 176 ++++++++++++++++++ .../value/sync/ModularSyncManager.java | 83 +++++++-- .../value/sync/PanelSyncHandler.java | 2 +- .../value/sync/PanelSyncManager.java | 170 +++-------------- .../cleanroommc/modularui/widget/Widget.java | 3 +- 9 files changed, 301 insertions(+), 174 deletions(-) create mode 100644 src/main/java/com/cleanroommc/modularui/value/sync/ISyncRegistrar.java diff --git a/src/main/java/com/cleanroommc/modularui/factory/GuiManager.java b/src/main/java/com/cleanroommc/modularui/factory/GuiManager.java index ffeea7481..27c3bdced 100644 --- a/src/main/java/com/cleanroommc/modularui/factory/GuiManager.java +++ b/src/main/java/com/cleanroommc/modularui/factory/GuiManager.java @@ -12,6 +12,7 @@ import com.cleanroommc.modularui.screen.ModularPanel; import com.cleanroommc.modularui.screen.ModularScreen; import com.cleanroommc.modularui.screen.UISettings; +import com.cleanroommc.modularui.value.sync.ModularSyncManager; import com.cleanroommc.modularui.value.sync.PanelSyncManager; import com.cleanroommc.modularui.widget.WidgetTree; @@ -72,11 +73,12 @@ public static void open(@NotNull UIFactory factory, @NotN // create panel, collect sync handlers and create container UISettings settings = new UISettings(RecipeViewerSettings.DUMMY); settings.defaultCanInteractWith(factory, guiData); - PanelSyncManager syncManager = new PanelSyncManager(false); + ModularSyncManager msm = new ModularSyncManager(false); + PanelSyncManager syncManager = new PanelSyncManager(msm, true); ModularPanel panel = factory.createPanel(guiData, syncManager, settings); WidgetTree.collectSyncValues(syncManager, panel); ModularContainer container = settings.hasContainer() ? settings.createContainer() : factory.createContainer(); - container.construct(player, syncManager, settings, panel.getName(), guiData); + container.construct(player, msm, settings, panel.getName(), guiData); // sync to client player.getNextWindowId(); player.closeContainer(); @@ -96,13 +98,14 @@ public static void openFromClient(int windowId, @NotNull UIF T guiData = factory.readGuiData(player, data); UISettings settings = new UISettings(); settings.defaultCanInteractWith(factory, guiData); - PanelSyncManager syncManager = new PanelSyncManager(true); + ModularSyncManager msm = new ModularSyncManager(true); + PanelSyncManager syncManager = new PanelSyncManager(msm, true); ModularPanel panel = factory.createPanel(guiData, syncManager, settings); WidgetTree.collectSyncValues(syncManager, panel); ModularScreen screen = factory.createScreen(guiData, panel); screen.getContext().setSettings(settings); ModularContainer container = settings.hasContainer() ? settings.createContainer() : factory.createContainer(); - container.construct(player, syncManager, settings, panel.getName(), guiData); + container.construct(player, msm, settings, panel.getName(), guiData); IMuiScreen wrapper = factory.createScreenWrapper(container, screen); if (!(wrapper.getGuiScreen() instanceof GuiContainer guiContainer)) { throw new IllegalStateException("The wrapping screen must be a GuiContainer for synced GUIs!"); diff --git a/src/main/java/com/cleanroommc/modularui/screen/ModularContainer.java b/src/main/java/com/cleanroommc/modularui/screen/ModularContainer.java index d3fd20838..0b73e45ea 100644 --- a/src/main/java/com/cleanroommc/modularui/screen/ModularContainer.java +++ b/src/main/java/com/cleanroommc/modularui/screen/ModularContainer.java @@ -9,7 +9,6 @@ import com.cleanroommc.modularui.utils.item.ItemHandlerHelper; import com.cleanroommc.modularui.utils.item.SlotItemHandler; import com.cleanroommc.modularui.value.sync.ModularSyncManager; -import com.cleanroommc.modularui.value.sync.PanelSyncManager; import com.cleanroommc.modularui.widgets.slot.ModularSlot; import com.cleanroommc.modularui.widgets.slot.SlotGroup; @@ -63,10 +62,10 @@ public static ModularContainer getCurrent(EntityPlayer player) { public ModularContainer() {} @ApiStatus.Internal - public void construct(EntityPlayer player, PanelSyncManager panelSyncManager, UISettings settings, String mainPanelName, GuiData guiData) { + public void construct(EntityPlayer player, ModularSyncManager msm, UISettings settings, String mainPanelName, GuiData guiData) { this.player = player; - this.syncManager = new ModularSyncManager(this); - this.syncManager.construct(mainPanelName, panelSyncManager); + this.syncManager = msm; + this.syncManager.construct(this, mainPanelName); this.settings = settings; this.guiData = guiData; sortShiftClickSlots(); diff --git a/src/main/java/com/cleanroommc/modularui/screen/RichTooltip.java b/src/main/java/com/cleanroommc/modularui/screen/RichTooltip.java index 96084bd5b..6417cbea5 100644 --- a/src/main/java/com/cleanroommc/modularui/screen/RichTooltip.java +++ b/src/main/java/com/cleanroommc/modularui/screen/RichTooltip.java @@ -54,6 +54,19 @@ public RichTooltip() { parent(Area.ZERO); } + public void reset() { + clearText(); + this.pos = null; + this.tooltipBuilder = null; + this.showUpTimer = 0; + this.autoUpdate = false; + this.titleMargin = 0; + this.appliedMargin = true; + this.x = 0; + this.y = 0; + this.maxWidth = Integer.MAX_VALUE; + } + public RichTooltip parent(Consumer parent) { this.parent = parent; return this; diff --git a/src/main/java/com/cleanroommc/modularui/test/TestTile.java b/src/main/java/com/cleanroommc/modularui/test/TestTile.java index 4bb963d33..b440a5d97 100644 --- a/src/main/java/com/cleanroommc/modularui/test/TestTile.java +++ b/src/main/java/com/cleanroommc/modularui/test/TestTile.java @@ -152,7 +152,7 @@ public ModularPanel buildUI(PosGuiData guiData, PanelSyncManager syncManager, UI syncManager.syncValue("mixer_fluids", 0, SyncHandlers.fluidSlot(this.mixerFluids1)); syncManager.syncValue("mixer_fluids", 1, SyncHandlers.fluidSlot(this.mixerFluids2)); IntSyncValue cycleStateValue = new IntSyncValue(() -> this.cycleState, val -> this.cycleState = val); - syncManager.syncValue("cycle_state", cycleStateValue); + syncManager.getHyperVisor().syncValue("cycle_state", cycleStateValue); syncManager.syncValue("display_item", GenericSyncValue.forItem(() -> this.displayItem, null)); syncManager.bindPlayerInventory(guiData.getPlayer()); syncManager.syncValue("textFieldSyncer", SyncHandlers.doubleNumber(() -> this.doubleValue, val -> this.doubleValue = val)); @@ -528,6 +528,7 @@ public ModularPanel openSecondWindow(PanelSyncManager syncManager, IPanelHandler syncManager.syncValue("int_value", new IntSyncValue(number::get, number::set)); IPanelHandler panelSyncHandler = syncManager.syncedPanel("other_panel_2", true, (syncManager1, syncHandler1) -> openThirdWindow(syncManager1, syncHandler1, number)); + IntSyncValue num = syncManager.getHyperVisor().findSyncHandler("cycle_state", IntSyncValue.class); panel.child(ButtonWidget.panelCloseButton()) .child(new ButtonWidget<>() .size(10).top(14).right(4) @@ -545,6 +546,13 @@ public ModularPanel openSecondWindow(PanelSyncManager syncManager, IPanelHandler .key('I', i -> new ItemSlot().slot(new ModularSlot(smallInv, i).slotGroup(slotGroup))) .build() .center()) + .child(new CycleButtonWidget() + .size(16).pos(5, 5 + 11) + .value(num) + .stateOverlay(0, IKey.str("1")) + .stateOverlay(1, IKey.str("2")) + .stateOverlay(2, IKey.str("3")) + .addTooltipLine(IKey.str("Hyper Visor test"))) .child(new ButtonWidget<>() .bottom(5) .right(5) diff --git a/src/main/java/com/cleanroommc/modularui/value/sync/ISyncRegistrar.java b/src/main/java/com/cleanroommc/modularui/value/sync/ISyncRegistrar.java new file mode 100644 index 000000000..53ee033f1 --- /dev/null +++ b/src/main/java/com/cleanroommc/modularui/value/sync/ISyncRegistrar.java @@ -0,0 +1,176 @@ +package com.cleanroommc.modularui.value.sync; + +import com.cleanroommc.modularui.api.IPanelHandler; +import com.cleanroommc.modularui.api.ISyncedAction; +import com.cleanroommc.modularui.widgets.slot.ModularSlot; +import com.cleanroommc.modularui.widgets.slot.SlotGroup; + +import net.minecraft.entity.player.EntityPlayer; +import net.minecraftforge.fml.relauncher.Side; +import net.minecraftforge.items.wrapper.PlayerMainInvWrapper; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.NoSuchElementException; +import java.util.function.Supplier; + +public interface ISyncRegistrar> { + + default S syncValue(String name, SyncHandler syncHandler) { + return syncValue(name, 0, syncHandler); + } + + S syncValue(String name, int id, SyncHandler syncHandler); + + default S syncValue(int id, SyncHandler syncHandler) { + return syncValue("_", id, syncHandler); + } + + default S itemSlot(String key, ModularSlot slot) { + return itemSlot(key, 0, slot); + } + + default S itemSlot(String key, int id, ModularSlot slot) { + return syncValue(key, id, new ItemSlotSH(slot)); + } + + default S itemSlot(int id, ModularSlot slot) { + return itemSlot("_", id, slot); + } + + default DynamicSyncHandler dynamicSyncHandler(String key, DynamicSyncHandler.IWidgetProvider widgetProvider) { + return dynamicSyncHandler(key, 0, widgetProvider); + } + + default DynamicSyncHandler dynamicSyncHandler(String key, int id, DynamicSyncHandler.IWidgetProvider widgetProvider) { + DynamicSyncHandler syncHandler = new DynamicSyncHandler().widgetProvider(widgetProvider); + syncValue(key, id, syncHandler); + return syncHandler; + } + + IPanelHandler syncedPanel(String key, boolean subPanel, PanelSyncHandler.IPanelBuilder panelBuilder); + + @Nullable IPanelHandler findPanelHandlerNullable(String key); + + default @NotNull IPanelHandler findPanelHandler(String key) { + IPanelHandler panelHandler = findPanelHandlerNullable(key); + if (panelHandler == null) { + throw new NoSuchElementException("Expected to find panel sync handler with key '" + key + "', but none was found."); + } + return panelHandler; + } + + S registerSlotGroup(SlotGroup slotGroup); + + default S registerSlotGroup(String name, int rowSize, int shiftClickPriority) { + return registerSlotGroup(new SlotGroup(name, rowSize, shiftClickPriority, true)); + } + + default S registerSlotGroup(String name, int rowSize, boolean allowShiftTransfer) { + return registerSlotGroup(new SlotGroup(name, rowSize, 100, allowShiftTransfer)); + } + + default S registerSlotGroup(String name, int rowSize) { + return registerSlotGroup(new SlotGroup(name, rowSize, 100, true)); + } + + default S bindPlayerInventory(EntityPlayer player) { + return bindPlayerInventory(player, ModularSlot::new); + } + + default S bindPlayerInventory(EntityPlayer player, @NotNull PanelSyncManager.SlotFunction slotFunction) { + if (getSlotGroup(ModularSyncManager.PLAYER_INVENTORY) != null) { + throw new IllegalStateException("The player slot group is already registered!"); + } + PlayerMainInvWrapper playerInventory = new PlayerMainInvWrapper(player.inventory); + String key = "player"; + for (int i = 0; i < 36; i++) { + itemSlot(key, i, slotFunction.apply(playerInventory, i).slotGroup(ModularSyncManager.PLAYER_INVENTORY)); + } + // player inv sorting is handled by bogosorter + registerSlotGroup(new SlotGroup(ModularSyncManager.PLAYER_INVENTORY, 9, SlotGroup.PLAYER_INVENTORY_PRIO, true).setAllowSorting(false)); + return (S) this; + } + + default S registerSyncedAction(String mapKey, ISyncedAction action) { + return registerSyncedAction(mapKey, true, true, action); + } + + default S registerSyncedAction(String mapKey, Side side, ISyncedAction action) { + return registerSyncedAction(mapKey, side.isClient(), side.isServer(), action); + } + + default S registerClientSyncedAction(String mapKey, ISyncedAction action) { + return registerSyncedAction(mapKey, true, false, action); + } + + default S registerServerSyncedAction(String mapKey, ISyncedAction action) { + return registerSyncedAction(mapKey, false, true, action); + } + + S registerSyncedAction(String mapKey, boolean executeClient, boolean executeServer, ISyncedAction action); + + default T getOrCreateSyncHandler(String name, Class clazz, Supplier supplier) { + return getOrCreateSyncHandler(name, 0, clazz, supplier); + } + + T getOrCreateSyncHandler(String name, int id, Class clazz, Supplier supplier); + + default ItemSlotSH getOrCreateSlot(String name, int id, Supplier slotSupplier) { + return getOrCreateSyncHandler(name, id, ItemSlotSH.class, () -> new ItemSlotSH(slotSupplier.get())); + } + + @Nullable SyncHandler findSyncHandlerNullable(String name, int id); + + default @Nullable SyncHandler findSyncHandlerNullable(String name) { + return findSyncHandlerNullable(name, 0); + } + + default @NotNull SyncHandler findSyncHandler(String name, int id) { + SyncHandler syncHandler = findSyncHandlerNullable(name, id); + if (syncHandler == null) { + throw new NoSuchElementException("Expected to find sync handler with key '" + makeSyncKey(name, id) + "', but none was found."); + } + return syncHandler; + } + + default @NotNull SyncHandler findSyncHandler(String name) { + return findSyncHandler(name, 0); + } + + default @Nullable T findSyncHandlerNullable(String name, int id, Class type) { + SyncHandler syncHandler = findSyncHandlerNullable(name, id); + if (syncHandler != null && type.isAssignableFrom(syncHandler.getClass())) { + return type.cast(syncHandler); + } + return null; + } + + default @Nullable T findSyncHandlerNullable(String name, Class type) { + return findSyncHandlerNullable(name, 0, type); + } + + default @NotNull T findSyncHandler(String name, int id, Class type) { + SyncHandler syncHandler = findSyncHandlerNullable(name, id); + if (syncHandler == null) { + throw new NoSuchElementException("Expected to find sync handler with key '" + makeSyncKey(name, id) + "', but none was found."); + } + if (!type.isAssignableFrom(syncHandler.getClass())) { + throw new ClassCastException("Expected to find sync handler with key '" + makeSyncKey(name, id) + "' of type '" + type.getName() + + "', but found type '" + syncHandler.getClass().getName() + "'."); + } + return type.cast(syncHandler); + } + + default @NotNull T findSyncHandler(String name, Class type) { + return findSyncHandler(name, 0, type); + } + + SlotGroup getSlotGroup(String name); + + static String makeSyncKey(String name, int id) { + return name + ":" + id; + } + +} diff --git a/src/main/java/com/cleanroommc/modularui/value/sync/ModularSyncManager.java b/src/main/java/com/cleanroommc/modularui/value/sync/ModularSyncManager.java index be455e228..d2cf91e0a 100644 --- a/src/main/java/com/cleanroommc/modularui/value/sync/ModularSyncManager.java +++ b/src/main/java/com/cleanroommc/modularui/value/sync/ModularSyncManager.java @@ -1,6 +1,8 @@ package com.cleanroommc.modularui.value.sync; import com.cleanroommc.modularui.ModularUI; +import com.cleanroommc.modularui.api.IPanelHandler; +import com.cleanroommc.modularui.api.ISyncedAction; import com.cleanroommc.modularui.screen.ModularContainer; import com.cleanroommc.modularui.utils.item.IItemHandler; import com.cleanroommc.modularui.utils.item.PlayerInvWrapper; @@ -22,39 +24,49 @@ import java.io.IOException; import java.util.Map; import java.util.Set; +import java.util.function.Supplier; -public class ModularSyncManager { +public class ModularSyncManager implements ISyncRegistrar { public static final String AUTO_SYNC_PREFIX = "auto_sync:"; protected static final String PLAYER_INVENTORY = "player_inventory"; - private static final String CURSOR_KEY = makeSyncKey("cursor_slot", 255255); + private static final String CURSOR_KEY = ISyncRegistrar.makeSyncKey("cursor_slot", 255255); private final Map panelSyncManagerMap = new Object2ObjectOpenHashMap<>(); // A set of all panels which have been opened during the ui. May also contain closed panels. // This is used to detect if packets are arriving too late private final Set panelHistory = new ObjectOpenHashSet<>(); private PanelSyncManager mainPSM; - private final ModularContainer container; + private ModularContainer container; private final CursorSlotSyncHandler cursorSlotSyncHandler = new CursorSlotSyncHandler(); + private final boolean client; - public ModularSyncManager(ModularContainer container) { - this.container = container; + public ModularSyncManager(boolean client) { + this.client = client; } - @ApiStatus.Internal - public void construct(String mainPanelName, PanelSyncManager mainPSM) { + void setMainPSM(PanelSyncManager mainPSM) { this.mainPSM = mainPSM; + } + + @ApiStatus.Internal + public void construct(ModularContainer container, String mainPanelName) { + this.container = container; if (this.mainPSM.getSlotGroup(PLAYER_INVENTORY) == null) { this.mainPSM.bindPlayerInventory(getPlayer()); } - mainPSM.syncValue(CURSOR_KEY, this.cursorSlotSyncHandler); - open(mainPanelName, mainPSM); + this.mainPSM.syncValue(CURSOR_KEY, this.cursorSlotSyncHandler); + open(mainPanelName, this.mainPSM); } public PanelSyncManager getMainPSM() { return mainPSM; } + public boolean isClient() { + return this.client; + } + public void detectAndSendChanges(boolean init) { this.panelSyncManagerMap.values().forEach(psm -> psm.detectAndSendChanges(init)); } @@ -98,7 +110,7 @@ public void setCursorItem(ItemStack item) { public void open(String name, PanelSyncManager syncManager) { this.panelSyncManagerMap.put(name, syncManager); this.panelHistory.add(name); - syncManager.initialize(name, this); + syncManager.initialize(name); } public void close(String name) { @@ -129,10 +141,6 @@ public ModularContainer getContainer() { return container; } - public boolean isClient() { - return this.container.isClient(); - } - private static boolean isPlayerSlot(Slot slot) { if (slot == null) return false; if (slot.inventory instanceof InventoryPlayer) { @@ -147,7 +155,52 @@ private static boolean isPlayerSlot(Slot slot) { return false; } + @Override + public ModularSyncManager syncValue(String name, int id, SyncHandler syncHandler) { + this.mainPSM.syncValue(name, id, syncHandler); + return this; + } + + @Override + public IPanelHandler syncedPanel(String key, boolean subPanel, PanelSyncHandler.IPanelBuilder panelBuilder) { + return this.mainPSM.syncedPanel(key, subPanel, panelBuilder); + } + + @Override + public @Nullable IPanelHandler findPanelHandlerNullable(String key) { + return this.mainPSM.findPanelHandlerNullable(key); + } + + @Override + public ModularSyncManager registerSlotGroup(SlotGroup slotGroup) { + this.mainPSM.registerSlotGroup(slotGroup); + return this; + } + + @Override + public ModularSyncManager registerSyncedAction(String mapKey, boolean executeClient, boolean executeServer, ISyncedAction action) { + this.mainPSM.registerSyncedAction(mapKey, executeClient, executeServer, action); + return this; + } + + @Override + public T getOrCreateSyncHandler(String name, int id, Class clazz, Supplier supplier) { + return this.mainPSM.getOrCreateSyncHandler(name, id, clazz, supplier); + } + + @Override + public @Nullable SyncHandler findSyncHandlerNullable(String name, int id) { + return this.mainPSM.findSyncHandlerNullable(name, id); + } + + @Override + public SlotGroup getSlotGroup(String name) { + return this.mainPSM.getSlotGroup(name); + } + + @ApiStatus.ScheduledForRemoval(inVersion = "3.2.0") + @Deprecated public static String makeSyncKey(String name, int id) { - return name + ":" + id; + return ISyncRegistrar.makeSyncKey(name, id); } } diff --git a/src/main/java/com/cleanroommc/modularui/value/sync/PanelSyncHandler.java b/src/main/java/com/cleanroommc/modularui/value/sync/PanelSyncHandler.java index c7a13ae59..171c6bc82 100644 --- a/src/main/java/com/cleanroommc/modularui/value/sync/PanelSyncHandler.java +++ b/src/main/java/com/cleanroommc/modularui/value/sync/PanelSyncHandler.java @@ -62,7 +62,7 @@ private void openPanel(boolean syncToServer) { if (this.syncManager != null && this.syncManager.getModularSyncManager() != getSyncManager().getModularSyncManager()) { throw new IllegalStateException("Can't reopen synced panel in another screen!"); } else if (this.syncManager == null) { - this.syncManager = new PanelSyncManager(client); + this.syncManager = new PanelSyncManager(getSyncManager().getModularSyncManager(), false); this.openedPanel = Objects.requireNonNull(createUI(this.syncManager)); this.panelName = this.openedPanel.getName(); this.openedPanel.setPanelSyncHandler(this); diff --git a/src/main/java/com/cleanroommc/modularui/value/sync/PanelSyncManager.java b/src/main/java/com/cleanroommc/modularui/value/sync/PanelSyncManager.java index 3a6765706..039fdcccc 100644 --- a/src/main/java/com/cleanroommc/modularui/value/sync/PanelSyncManager.java +++ b/src/main/java/com/cleanroommc/modularui/value/sync/PanelSyncManager.java @@ -29,19 +29,18 @@ import java.util.Collection; import java.util.List; import java.util.Map; -import java.util.NoSuchElementException; import java.util.Objects; import java.util.function.Consumer; import java.util.function.Supplier; -public class PanelSyncManager { +public class PanelSyncManager implements ISyncRegistrar { private final Map syncHandlers = new Object2ObjectLinkedOpenHashMap<>(); private final Map slotGroups = new Object2ObjectOpenHashMap<>(); private final Map reverseSyncHandlers = new Object2ObjectOpenHashMap<>(); private final Map syncedActions = new Object2ObjectOpenHashMap<>(); private final Map subPanels = new Object2ObjectArrayMap<>(); - private ModularSyncManager modularSyncManager; + private final ModularSyncManager modularSyncManager; private String panelName; private boolean init = true; private boolean locked = false; @@ -52,18 +51,20 @@ public class PanelSyncManager { private final List> closeListener = new ArrayList<>(); private final List tickListener = new ArrayList<>(); - public PanelSyncManager(boolean client) { - this.client = client; + @ApiStatus.Internal + public PanelSyncManager(ModularSyncManager msm, boolean main) { + this.modularSyncManager = msm; + this.client = msm.isClient(); + if (main) msm.setMainPSM(this); } @ApiStatus.Internal - public void initialize(String panelName, ModularSyncManager msm) { - this.modularSyncManager = msm; + public void initialize(String panelName) { this.panelName = panelName; this.syncHandlers.forEach((mapKey, syncHandler) -> syncHandler.init(mapKey, this)); this.locked = true; this.init = true; - this.subPanels.forEach((s, syncHandler) -> msm.getMainPSM().registerPanelSyncHandler(s, syncHandler)); + this.subPanels.forEach((s, syncHandler) -> this.modularSyncManager.getMainPSM().registerPanelSyncHandler(s, syncHandler)); } private void registerPanelSyncHandler(String name, SyncHandler syncHandler) { @@ -105,7 +106,7 @@ public void onClose() { } public boolean isInitialised() { - return this.modularSyncManager != null; + return this.panelName != null; } void detectAndSendChanges(boolean init) { @@ -200,10 +201,7 @@ private void putSyncValue(String name, int id, SyncHandler syncHandler) { } } - public PanelSyncManager syncValue(String name, SyncHandler syncHandler) { - return syncValue(name, 0, syncHandler); - } - + @Override public PanelSyncManager syncValue(String name, int id, SyncHandler syncHandler) { Objects.requireNonNull(name, "Name must not be null"); Objects.requireNonNull(syncHandler, "Sync Handler must not be null"); @@ -211,32 +209,6 @@ public PanelSyncManager syncValue(String name, int id, SyncHandler syncHandler) return this; } - public PanelSyncManager syncValue(int id, SyncHandler syncHandler) { - return syncValue("_", id, syncHandler); - } - - public PanelSyncManager itemSlot(String key, ModularSlot slot) { - return itemSlot(key, 0, slot); - } - - public PanelSyncManager itemSlot(String key, int id, ModularSlot slot) { - return syncValue(key, id, new ItemSlotSH(slot)); - } - - public PanelSyncManager itemSlot(int id, ModularSlot slot) { - return itemSlot("_", id, slot); - } - - public DynamicSyncHandler dynamicSyncHandler(String key, DynamicSyncHandler.IWidgetProvider widgetProvider) { - return dynamicSyncHandler(key, 0, widgetProvider); - } - - public DynamicSyncHandler dynamicSyncHandler(String key, int id, DynamicSyncHandler.IWidgetProvider widgetProvider) { - DynamicSyncHandler syncHandler = new DynamicSyncHandler().widgetProvider(widgetProvider); - syncValue(key, id, syncHandler); - return syncHandler; - } - /** * @deprecated replaced by {@link #syncedPanel(String, boolean, PanelSyncHandler.IPanelBuilder)} */ @@ -267,6 +239,7 @@ public IPanelHandler panel(String key, PanelSyncHandler.IPanelBuilder panelBuild * @throws IllegalArgumentException if the build panel of the builder is the main panel * @throws IllegalStateException if this method was called too late */ + @Override public IPanelHandler syncedPanel(String key, boolean subPanel, PanelSyncHandler.IPanelBuilder panelBuilder) { IPanelHandler ph = findPanelHandlerNullable(key); if (ph != null) return ph; @@ -284,18 +257,12 @@ public IPanelHandler syncedPanel(String key, boolean subPanel, PanelSyncHandler. return syncHandler; } + @Override public @Nullable IPanelHandler findPanelHandlerNullable(String key) { return this.subPanels.get(key); } - public @NotNull IPanelHandler findPanelHandler(String key) { - IPanelHandler panelHandler = findPanelHandlerNullable(key); - if (panelHandler == null) { - throw new NoSuchElementException("Expected to find panel sync handler with key '" + key + "', but none was found."); - } - return panelHandler; - } - + @Override public PanelSyncManager registerSlotGroup(SlotGroup slotGroup) { if (!slotGroup.isSingleton()) { this.slotGroups.put(slotGroup.getName(), slotGroup); @@ -303,36 +270,6 @@ public PanelSyncManager registerSlotGroup(SlotGroup slotGroup) { return this; } - public PanelSyncManager registerSlotGroup(String name, int rowSize, int shiftClickPriority) { - return registerSlotGroup(new SlotGroup(name, rowSize, shiftClickPriority, true)); - } - - public PanelSyncManager registerSlotGroup(String name, int rowSize, boolean allowShiftTransfer) { - return registerSlotGroup(new SlotGroup(name, rowSize, 100, allowShiftTransfer)); - } - - public PanelSyncManager registerSlotGroup(String name, int rowSize) { - return registerSlotGroup(new SlotGroup(name, rowSize, 100, true)); - } - - public PanelSyncManager bindPlayerInventory(EntityPlayer player) { - return bindPlayerInventory(player, ModularSlot::new); - } - - public PanelSyncManager bindPlayerInventory(EntityPlayer player, @NotNull SlotFunction slotFunction) { - if (getSlotGroup(ModularSyncManager.PLAYER_INVENTORY) != null) { - throw new IllegalStateException("The player slot group is already registered!"); - } - PlayerMainInvWrapper playerInventory = new PlayerMainInvWrapper(player.inventory); - String key = "player"; - for (int i = 0; i < 36; i++) { - itemSlot(key, i, slotFunction.apply(playerInventory, i).slotGroup(ModularSyncManager.PLAYER_INVENTORY)); - } - // player inv sorting is handled by bogosorter - registerSlotGroup(new SlotGroup(ModularSyncManager.PLAYER_INVENTORY, 9, SlotGroup.PLAYER_INVENTORY_PRIO, true).setAllowSorting(false)); - return this; - } - public interface SlotFunction { @NotNull @@ -376,22 +313,7 @@ public PanelSyncManager onCommonTick(Runnable runnable) { return this; } - public PanelSyncManager registerSyncedAction(String mapKey, ISyncedAction action) { - return registerSyncedAction(mapKey, true, true, action); - } - - public PanelSyncManager registerSyncedAction(String mapKey, Side side, ISyncedAction action) { - return registerSyncedAction(mapKey, side.isClient(), side.isServer(), action); - } - - public PanelSyncManager registerClientSyncedAction(String mapKey, ISyncedAction action) { - return registerSyncedAction(mapKey, true, false, action); - } - - public PanelSyncManager registerServerSyncedAction(String mapKey, ISyncedAction action) { - return registerSyncedAction(mapKey, false, true, action); - } - + @Override public PanelSyncManager registerSyncedAction(String mapKey, boolean executeClient, boolean executeServer, ISyncedAction action) { if (executeClient || executeServer) { this.syncedActions.put(mapKey, new SyncedAction(action, executeClient, executeServer)); @@ -416,10 +338,7 @@ public void callSyncedAction(String mapKey, Consumer packetBuilder callSyncedAction(mapKey, packet); } - public T getOrCreateSyncHandler(String name, Class clazz, Supplier supplier) { - return getOrCreateSyncHandler(name, 0, clazz, supplier); - } - + @Override public T getOrCreateSyncHandler(String name, int id, Class clazz, Supplier supplier) { SyncHandler syncHandler = findSyncHandlerNullable(name, id); if (syncHandler == null) { @@ -440,10 +359,7 @@ public T getOrCreateSyncHandler(String name, int id, Cla throw new IllegalStateException("SyncHandler for key " + makeSyncKey(name, id) + " is of type " + syncHandler.getClass() + ", but type " + clazz + " was expected!"); } - public ItemSlotSH getOrCreateSlot(String name, int id, Supplier slotSupplier) { - return getOrCreateSyncHandler(name, id, ItemSlotSH.class, () -> new ItemSlotSH(slotSupplier.get())); - } - + @Override public SlotGroup getSlotGroup(String name) { return this.slotGroups.get(name); } @@ -462,65 +378,23 @@ public Collection getSlotGroups() { return this.syncHandlers.get(mapKey); } + @Override public @Nullable SyncHandler findSyncHandlerNullable(String name, int id) { return this.syncHandlers.get(makeSyncKey(name, id)); } - public @Nullable SyncHandler findSyncHandlerNullable(String name) { - return findSyncHandlerNullable(name, 0); - } - - public @NotNull SyncHandler findSyncHandler(String name, int id) { - SyncHandler syncHandler = this.syncHandlers.get(makeSyncKey(name, id)); - if (syncHandler == null) { - throw new NoSuchElementException("Expected to find sync handler with key '" + makeSyncKey(name, id) + "', but none was found."); - } - return syncHandler; - } - - public @NotNull SyncHandler findSyncHandler(String name) { - return findSyncHandler(name, 0); - } - - public @Nullable T findSyncHandlerNullable(String name, int id, Class type) { - SyncHandler syncHandler = this.syncHandlers.get(makeSyncKey(name, id)); - if (syncHandler != null && type.isAssignableFrom(syncHandler.getClass())) { - return type.cast(syncHandler); - } - return null; - } - - public @Nullable T findSyncHandlerNullable(String name, Class type) { - return findSyncHandlerNullable(name, 0, type); - } - - public @NotNull T findSyncHandler(String name, int id, Class type) { - SyncHandler syncHandler = this.syncHandlers.get(makeSyncKey(name, id)); - if (syncHandler == null) { - throw new NoSuchElementException("Expected to find sync handler with key '" + makeSyncKey(name, id) + "', but none was found."); - } - if (!type.isAssignableFrom(syncHandler.getClass())) { - throw new ClassCastException("Expected to find sync handler with key '" + makeSyncKey(name, id) + "' of type '" + type.getName() - + "', but found type '" + syncHandler.getClass().getName() + "'."); - } - return type.cast(syncHandler); - } - - public @NotNull T findSyncHandler(String name, Class type) { - return findSyncHandler(name, 0, type); - } - public EntityPlayer getPlayer() { return getModularSyncManager().getPlayer(); } public ModularSyncManager getModularSyncManager() { - if (!isInitialised()) { - throw new IllegalStateException("PanelSyncManager is not yet initialised!"); - } return modularSyncManager; } + public ISyncRegistrar getHyperVisor() { + return this.modularSyncManager.getMainPSM(); + } + public ModularContainer getContainer() { return getModularSyncManager().getContainer(); } diff --git a/src/main/java/com/cleanroommc/modularui/widget/Widget.java b/src/main/java/com/cleanroommc/modularui/widget/Widget.java index a29f2c5e2..734eec676 100644 --- a/src/main/java/com/cleanroommc/modularui/widget/Widget.java +++ b/src/main/java/com/cleanroommc/modularui/widget/Widget.java @@ -21,6 +21,7 @@ import com.cleanroommc.modularui.theme.WidgetTheme; import com.cleanroommc.modularui.theme.WidgetThemeEntry; import com.cleanroommc.modularui.theme.WidgetThemeKey; +import com.cleanroommc.modularui.value.sync.ISyncRegistrar; import com.cleanroommc.modularui.value.sync.ModularSyncManager; import com.cleanroommc.modularui.value.sync.SyncHandler; import com.cleanroommc.modularui.value.sync.ValueSyncHandler; @@ -843,7 +844,7 @@ public boolean isSynced() { */ @Override public W syncHandler(String name, int id) { - this.syncKey = ModularSyncManager.makeSyncKey(name, id); + this.syncKey = ISyncRegistrar.makeSyncKey(name, id); return getThis(); } From cae522493a3ba9366e7ec407ae30d1d08335faee Mon Sep 17 00:00:00 2001 From: brachy84 Date: Sun, 7 Dec 2025 11:42:52 +0100 Subject: [PATCH 14/50] update readme (cherry picked from commit 874dc5c153e3cd45f192a4b1c570eeac37014ea8) --- README.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 05bc614a5..4e866ecbe 100644 --- a/README.md +++ b/README.md @@ -14,11 +14,17 @@ With ModularUI you simply call `.child(new FluidSlot().syncHandler(new FluidTank - panel system similar to windows - widgets are placed in a tree like structure - widget rendering and interactions are automatically handled +- no need to create GUI texture sheets, each widget is rendered dynamically - easy and dynamic widget sizing and positioning -- syncing widget values +- build in APIs for various UI things like color, stencil (fancy scissor) and animations +- easy syncing between client and server without - good for client only GUIs and client-server synced GUIs - GUI themes are loaded via JSON and can be added and modified by resourcepacks -- NEI compat for things like exclusion zones +- NEI compat for things like exclusion zones and ghost ingredients + +## Planned Features +- Create NEI recipe handlers with MUI +- Improved text rendering ### History - First appearance of ModularUI in GTCE by Archengius From 1d7a461bf52f3fd125b5b325f60d826c5bd39af8 Mon Sep 17 00:00:00 2001 From: brachy84 Date: Sun, 7 Dec 2025 13:01:52 +0100 Subject: [PATCH 15/50] adjust post the log anim & other (cherry picked from commit 425150dc3bd1c7662f6120cb171d0bc40802f20b) --- .../modularui/drawable/GuiDraw.java | 10 ++++++++- .../cleanroommc/modularui/test/TestGuis.java | 21 +++++++------------ .../cleanroommc/modularui/utils/Color.java | 2 +- 3 files changed, 17 insertions(+), 16 deletions(-) diff --git a/src/main/java/com/cleanroommc/modularui/drawable/GuiDraw.java b/src/main/java/com/cleanroommc/modularui/drawable/GuiDraw.java index b5a50e74f..417fad473 100644 --- a/src/main/java/com/cleanroommc/modularui/drawable/GuiDraw.java +++ b/src/main/java/com/cleanroommc/modularui/drawable/GuiDraw.java @@ -397,12 +397,20 @@ public static void drawTiledSprite(TextureAtlasSprite sprite, float x0, float y0 drawTiledSprite(Minecraft.getMinecraft().getTextureMapBlocks(), sprite, x0, y0, w, h); } + public static void drawTiledSprite(TextureAtlasSprite sprite, float x0, float y0, float w, float h, int tileWidth, int tileHeight) { + drawTiledSprite(Minecraft.getMinecraft().getTextureMapBlocks(), sprite, x0, y0, w, h, tileWidth, tileHeight); + } + public static void drawTiledSprite(TextureMap textureMap, TextureAtlasSprite sprite, float x0, float y0, float w, float h) { + drawTiledSprite(textureMap, sprite, x0, y0, w, h, sprite.getIconWidth(), sprite.getIconHeight()); + } + + public static void drawTiledSprite(TextureMap textureMap, TextureAtlasSprite sprite, float x0, float y0, float w, float h, int tileWidth, int tileHeight) { GlStateManager.disableAlpha(); GlStateManager.enableBlend(); GlStateManager.enableTexture2D(); GlStateManager.bindTexture(textureMap.getGlTextureId()); - drawTiledTexture(x0, y0, x0 + w, y0 + h, sprite.getMinU(), sprite.getMinV(), sprite.getMaxU(), sprite.getMaxV(), sprite.getIconWidth(), sprite.getIconHeight(), 0); + drawTiledTexture(x0, y0, x0 + w, y0 + h, sprite.getMinU(), sprite.getMinV(), sprite.getMaxU(), sprite.getMaxV(), tileWidth, tileHeight, 0); GlStateManager.disableBlend(); GlStateManager.enableAlpha(); }*/ diff --git a/src/main/java/com/cleanroommc/modularui/test/TestGuis.java b/src/main/java/com/cleanroommc/modularui/test/TestGuis.java index df2a53ab7..6d7247b51 100644 --- a/src/main/java/com/cleanroommc/modularui/test/TestGuis.java +++ b/src/main/java/com/cleanroommc/modularui/test/TestGuis.java @@ -186,28 +186,21 @@ public void onInit() { public static @NotNull ModularPanel buildPostTheLogAnimationUI() { Animator post = new Animator().curve(Interpolation.SINE_IN).duration(300).bounds(-35, 0); Animator the = new Animator().curve(Interpolation.SINE_IN).duration(300).bounds(-20, 0); - Animator fucking = new Animator().curve(Interpolation.SINE_IN).duration(300).bounds(53, 0); + Animator extraordinary = new Animator().curve(Interpolation.SINE_IN).duration(500).bounds(53, 0); Animator log = new Animator().curve(Interpolation.SINE_IN).duration(300).bounds(20, 0); Animator logGrow = new Animator().curve(Interpolation.LINEAR).duration(2500).bounds(0f, 1f); IAnimator animator = new Wait(300) .followedBy(post) .followedBy(the) - .followedBy(fucking) + .followedBy(extraordinary) .followedBy(log) .followedBy(logGrow); animator.animate(); Random rnd = new Random(); - /*TextureAtlasSprite[] sprites = IntStream.range(0, 10).mapToObj(SpriteHelper::getDestroyBlockSprite).toArray(TextureAtlasSprite[]::new); - IDrawable broken = ((context1, x, y, width, height, widgetTheme) -> { - if (logGrow.getValue() < 0.1f) return; - GlStateManager.color(1f, 1f, 1f, 0.75f); - GuiDraw.drawTiledSprite(sprites[(int) Math.min(9, logGrow.getValue() * 10)], x, y, width + 24, height + 24); - });*/ return new ModularPanel("main") .coverChildren() - .padding(12) - //.overlay(broken) .child(new Column() + .margin(12) .coverChildren() .child(new Row() .coverChildren() @@ -215,9 +208,9 @@ public void onInit() { .transform((widget, stack) -> stack.translate(post.getValue(), 0))) .child(IKey.str("the ").asWidget() .transform((widget, stack) -> stack.translate(0, the.getValue()))) - .child(IKey.str("fucking ").asWidget() - .transform((widget, stack) -> stack.translate(fucking.getValue(), 0)))) - .child(IKey.str("LOOOOGG!!!! ").asWidget() + .child(IKey.str("fucking ").style(TextFormatting.OBFUSCATED).asWidget() + .transform((widget, stack) -> stack.translate(extraordinary.getValue(), 0)))) + .child(IKey.str("LOOOOGG!!!!").asWidget() .paddingTop(4) .transform((widget, stack) -> { float logVal = log.getValue(); @@ -228,7 +221,7 @@ public void onInit() { stack.translate(x0, y0); stack.scale(scale, scale); stack.translate(-x0, -y0); - widget.color(Color.interpolate(0xFF040404, Color.RED.main, Math.min(1f, 1.2f * logGrowVal))); + widget.color(Color.lerp(0xFF040404, Color.RED.main, Math.min(1f, 1.2f * logGrowVal))); }))); } diff --git a/src/main/java/com/cleanroommc/modularui/utils/Color.java b/src/main/java/com/cleanroommc/modularui/utils/Color.java index e116f5c4a..3b947dc4c 100644 --- a/src/main/java/com/cleanroommc/modularui/utils/Color.java +++ b/src/main/java/com/cleanroommc/modularui/utils/Color.java @@ -777,7 +777,7 @@ public static int interpolate(IInterpolation curve, int color1, int color2, floa int r = (int) lerpComp(curve, Color.getRed(color1), Color.getRed(color2), value); int g = (int) lerpComp(curve, Color.getGreen(color1), Color.getGreen(color2), value); int b = (int) lerpComp(curve, Color.getBlue(color1), Color.getBlue(color2), value); - int a = (int) lerpComp(curve, Color.getAlpha(color1), Color.getAlpha(color2), value); + int a = Interpolations.lerp(Color.getAlpha(color1), Color.getAlpha(color2), value); return Color.argb(r, g, b, a); } From 88fc32cc12b20f00360fffac40901c530cbf7165 Mon Sep 17 00:00:00 2001 From: brachy84 Date: Sun, 7 Dec 2025 21:25:45 +0100 Subject: [PATCH 16/50] helper class to read image sizes fast (cherry picked from commit 76675c193773d7d57100f0e3daac3323956678db) --- .../modularui/utils/ImageUtil.java | 263 ++++++++++++++++++ 1 file changed, 263 insertions(+) create mode 100644 src/main/java/com/cleanroommc/modularui/utils/ImageUtil.java diff --git a/src/main/java/com/cleanroommc/modularui/utils/ImageUtil.java b/src/main/java/com/cleanroommc/modularui/utils/ImageUtil.java new file mode 100644 index 000000000..0ef65f54b --- /dev/null +++ b/src/main/java/com/cleanroommc/modularui/utils/ImageUtil.java @@ -0,0 +1,263 @@ +package com.cleanroommc.modularui.utils; + +import com.cleanroommc.modularui.ModularUI; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.resources.IResource; +import net.minecraft.util.ResourceLocation; + +import java.io.DataInput; +import java.io.DataInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; + +/** + * A helper class to parse image size from file bytes directly without reading the whole file. + * Supported file types are PNG, JPEG and GIF. The implementation is fast and avoids any unnecessary computation. + */ +public class ImageUtil { + + private static final List TYPES = new ArrayList<>(); + private static final byte[] buffer = new byte[256]; + + public static final long ERROR_NO_RESOURCE = -1; + public static final long ERROR_NO_IMAGE_TYPE = -2; + public static final long ERROR_IO_EXCEPTION = -3; + public static final long ERROR_PNG = -4; + public static final long ERROR_JPEG_1 = -5; + public static final long ERROR_JPEG_2 = -6; + + private static final String[] ERROR_MSG = { + "Resource not found", + "Unsupported file type", + "Failed to parse image with unknown cause", + "PNG file ended too early", + "JPEG started again within itself", + "JPEG ended too early" + }; + + public static IResource getResource(ResourceLocation resLoc) { + try { + return Minecraft.getMinecraft().getResourceManager().getResource(resLoc); + } catch (IOException e) { + return null; + } + } + + /** + * Parses the image size from a resource. The returned packed size can be unpacked with {@link #getWidth(long)} and + * {@link #getHeight(long)}. If it failed a negative value is returned. Negative values can be translated into an error + * message using {@link #getError(long)}. + * + * @param resLoc resource location of the image + * @return packed size or negative error value + */ + public static long readImageSize(ResourceLocation resLoc) { + IResource res = getResource(resLoc); + return res == null ? ERROR_NO_RESOURCE : readImageSize(res); + } + + public static String getError(long size) { + return size < 0 ? ERROR_MSG[(int) (-size - 1)] : null; + } + + /** + * Parses the image size from a resource. The returned packed size can be unpacked with {@link #getWidth(long)} and + * {@link #getHeight(long)}. If it failed a negative value is returned. Negative values can be translated into an error + * message using {@link #getError(long)}. A resource can be obtained with {@link #getResource(ResourceLocation)}. + * + * @param resource resource to read image size from + * @return packed size or negative error value + */ + public static long readImageSize(IResource resource) { + try (InputStream inputStream = resource.getInputStream()) { + return readImageSize(inputStream); + } catch (IOException e) { + return ERROR_IO_EXCEPTION; + } + } + + /** + * Parses the image size from the input stream. The returned packed size can be unpacked with {@link #getWidth(long)} and + * {@link #getHeight(long)}. If it failed a negative value is returned or an exception is thrown. + * Negative values can be translated into an error message using {@link #getError(long)}. + * + * @param inputStream bytes to read from + * @return packed size or negative error value + * @throws IOException if the input stream is not a valid image file + */ + public static long readImageSize(InputStream inputStream) throws IOException { + ImageType type = parseImageType(inputStream); + return type == null ? ERROR_NO_IMAGE_TYPE : type.parse(inputStream); + } + + public static long packSize(int width, int height) { + return width | (long) height << 32; + } + + public static int getWidth(long packedSize) { + return (int) (packedSize & 0xFFFFFFFFL); + } + + public static int getHeight(long packedSize) { + return (int) ((packedSize >> 32) & 0xFFFFFFFFL); + } + + public static boolean testImageSize(ResourceLocation resLoc, int width, int height) { + long size = ImageUtil.readImageSize(resLoc); + if (size < 0) { + ModularUI.LOGGER.error("{} for location '{}'", getError(size), resLoc); + return false; + } + int w = ImageUtil.getWidth(size); + int h = ImageUtil.getHeight(size); + if (w != width || h != height) { + ModularUI.LOGGER.error("Image size is incorrect of image '{}'. Expected {}|{}, but actually is {}|{}", resLoc, width, height, w, h); + } else { + ModularUI.LOGGER.info("Image '{}' has correct size", resLoc); + } + return true; + } + + private static ImageType parseImageType(InputStream inputStream) throws IOException { + if (TYPES.isEmpty()) initImageTypes(); + int bytesRead = 0; + for (ImageType type : TYPES) { + while (type.signatureLength > bytesRead) { + buffer[bytesRead] = (byte) inputStream.read(); + bytesRead++; + } + if (startsWith(buffer, bytesRead, type.signatureStart)) { + return type; + } + } + return null; + } + + public static String getImageType(InputStream inputStream) { + try { + ImageType type = parseImageType(inputStream); + return type == null ? null : type.name(); + } catch (IOException e) { + return null; + } + } + + private static void initImageTypes() { + // puts image types into the list sorted by signature length + // this allows checking for all image types without using PushbackInputStream + TYPES.clear(); + ImageType[] types = ImageType.values(); + TYPES.add(types[0]); + for (int i = 1; i < types.length; i++) { + for (int j = 0; j < TYPES.size(); j++) { + if (TYPES.get(j).signatureLength > types[i].signatureLength) { + TYPES.add(j, types[i]); + break; + } + } + } + } + + public static DataInput getDataInput(InputStream is) { + return is instanceof DataInput dataInput ? dataInput : new DataInputStream(is); + } + + public static byte[] toBytes(int... ints) { + byte[] bytes = new byte[ints.length]; + for (int i = 0; i < ints.length; i++) { + bytes[i] = (byte) (ints[i] & 0xFF); + } + return bytes; + } + + private static boolean startsWith(byte[] bytes, int bytesLen, byte[] startsWith) { + if (startsWith.length > bytesLen) return false; + for (int i = 0; i < startsWith.length; i++) { + if (startsWith[i] != bytes[i]) return false; + } + return true; + } + + public static String getFileTypeOfPath(String path) { + int i = path.lastIndexOf('.'); + return i < 0 || i == path.length() - 1 ? null : path.substring(i + 1); + } + + public static int readLittleEndianShort(InputStream inputStream) throws IOException { + // DataInputStream reads big endian shorts + return (inputStream.read() & 0xFF) | ((inputStream.read() & 0xFF) << 8); + } + + private interface SizeParser { + + long parse(InputStream inputStream) throws IOException; + } + + private enum ImageType implements SizeParser { + PNG(8, 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A) { + @Override + public long parse(InputStream inputStream) throws IOException { + DataInput dataInput = getDataInput(inputStream); + int skipped = dataInput.skipBytes(8); // IHDR length (4), IHDR type (4) + if (skipped != 8) return ERROR_PNG; + return packSize(dataInput.readInt(), dataInput.readInt()); // width and height + } + }, + JPEG(2, 0xFF, 0xD8) { + @Override + public long parse(InputStream inputStream) throws IOException { + DataInput dis = getDataInput(inputStream); + + while (true) { + int marker = dis.readUnsignedShort(); + + // Skip padding FFs + while (marker == 0xFFFF) { + marker = dis.readUnsignedByte(); + } + + // SOFn markers that contain size: + if (marker >= 0xFFC0 && marker <= 0xFFC3 || + marker >= 0xFFC5 && marker <= 0xFFC7 || + marker >= 0xFFC9 && marker <= 0xFFCB || + marker >= 0xFFCD && marker <= 0xFFCF) { + + dis.readUnsignedShort(); // block length + dis.readUnsignedByte(); // sample precision (8 bits) + int height = dis.readUnsignedShort(); + int width = dis.readUnsignedShort(); + return packSize(width, height); + } + + if (marker == 0xFFD8) return ERROR_JPEG_1; + if (marker == 0xFFD9) return ERROR_JPEG_2; + if (marker >= 0xFFD0 && marker <= 0xFFD7 || marker == 0xFF01) continue; // no payload + + // read length of payload and skip it + // length includes marker, hence -2 + int len = dis.readUnsignedShort(); + dis.skipBytes(len - 2); + } + } + }, + // 5th byte is variable -> only use the first 4 + GIF(6, 0x47, 0x49, 0x46, 0x38) { + @Override + public long parse(InputStream inputStream) throws IOException { + return packSize(readLittleEndianShort(inputStream), readLittleEndianShort(inputStream)); + } + }; + + + private final int signatureLength; + private final byte[] signatureStart; + + ImageType(int signatureLength, int... signatureStart) { + this.signatureLength = signatureLength; + this.signatureStart = toBytes(signatureStart); + } + } +} From fcd11b14cb67215a1ac2d35c84569300a69b67ad Mon Sep 17 00:00:00 2001 From: brachy84 Date: Sun, 7 Dec 2025 23:19:29 +0100 Subject: [PATCH 17/50] hollow rectangles and fluent rectangle setter names (cherry picked from commit 7f8be82d850bb3b12a3facc4e33e56e45cf9d917) --- .../modularui/drawable/Circle.java | 4 +- .../modularui/drawable/Rectangle.java | 134 +++++++++++++++--- .../cleanroommc/modularui/test/GLTestGui.java | 2 +- .../cleanroommc/modularui/test/TestGuis.java | 14 +- .../cleanroommc/modularui/test/TestTile.java | 6 +- .../modularui/widgets/ColorPickerDialog.java | 16 +-- .../modularui/widgets/SliderWidget.java | 2 +- 7 files changed, 139 insertions(+), 39 deletions(-) diff --git a/src/main/java/com/cleanroommc/modularui/drawable/Circle.java b/src/main/java/com/cleanroommc/modularui/drawable/Circle.java index a65b1b64d..112db958d 100644 --- a/src/main/java/com/cleanroommc/modularui/drawable/Circle.java +++ b/src/main/java/com/cleanroommc/modularui/drawable/Circle.java @@ -91,8 +91,8 @@ public boolean saveToJson(JsonObject json) { @Override public Circle interpolate(Circle start, Circle end, float t) { - this.colorInner = Color.interpolate(start.colorInner, end.colorInner, t); - this.colorOuter = Color.interpolate(start.colorOuter, end.colorOuter, t); + this.colorInner = Color.lerp(start.colorInner, end.colorInner, t); + this.colorOuter = Color.lerp(start.colorOuter, end.colorOuter, t); this.segments = Interpolations.lerp(start.segments, end.segments, t); return this; } diff --git a/src/main/java/com/cleanroommc/modularui/drawable/Rectangle.java b/src/main/java/com/cleanroommc/modularui/drawable/Rectangle.java index 056f50d83..cd8d2b827 100644 --- a/src/main/java/com/cleanroommc/modularui/drawable/Rectangle.java +++ b/src/main/java/com/cleanroommc/modularui/drawable/Rectangle.java @@ -1,5 +1,6 @@ package com.cleanroommc.modularui.drawable; +import com.cleanroommc.modularui.ModularUI; import com.cleanroommc.modularui.animation.IAnimatable; import com.cleanroommc.modularui.api.IJsonSerializable; import com.cleanroommc.modularui.api.drawable.IDrawable; @@ -8,24 +9,25 @@ import com.cleanroommc.modularui.utils.Color; import com.cleanroommc.modularui.utils.Interpolations; import com.cleanroommc.modularui.utils.JsonHelper; +import com.cleanroommc.modularui.utils.Platform; import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; import com.google.gson.JsonElement; import com.google.gson.JsonObject; +import org.jetbrains.annotations.ApiStatus; import java.util.function.IntConsumer; public class Rectangle implements IDrawable, IJsonSerializable, IAnimatable { - public static final double PI_2 = Math.PI / 2; - private int cornerRadius, colorTL, colorTR, colorBL, colorBR, cornerSegments; + private float borderThickness; private boolean canApplyTheme = false; public Rectangle() { - setColor(0xFFFFFFFF); + color(0xFFFFFFFF); this.cornerRadius = 0; this.cornerSegments = 6; } @@ -34,12 +36,27 @@ public int getColor() { return this.colorTL; } + @ApiStatus.ScheduledForRemoval(inVersion = "3.2.0") + @Deprecated public Rectangle setCornerRadius(int cornerRadius) { + return cornerRadius(cornerRadius); + } + + public Rectangle cornerRadius(int cornerRadius) { this.cornerRadius = Math.max(0, cornerRadius); + if (this.borderThickness > 0 && this.cornerRadius > 0) { + ModularUI.LOGGER.error("Hollow rectangles currently can't have a corner radius."); + } return this; } + @ApiStatus.ScheduledForRemoval(inVersion = "3.2.0") + @Deprecated public Rectangle setColor(int colorTL, int colorTR, int colorBL, int colorBR) { + return color(colorTL, colorTR, colorBL, colorBR); + } + + public Rectangle color(int colorTL, int colorTR, int colorBL, int colorBR) { this.colorTL = colorTL; this.colorTR = colorTR; this.colorBL = colorBL; @@ -47,37 +64,108 @@ public Rectangle setColor(int colorTL, int colorTR, int colorBL, int colorBR) { return this; } + @ApiStatus.ScheduledForRemoval(inVersion = "3.2.0") + @Deprecated public Rectangle setVerticalGradient(int colorTop, int colorBottom) { - return setColor(colorTop, colorTop, colorBottom, colorBottom); + return verticalGradient(colorTop, colorBottom); + } + + public Rectangle verticalGradient(int colorTop, int colorBottom) { + return color(colorTop, colorTop, colorBottom, colorBottom); } + @ApiStatus.ScheduledForRemoval(inVersion = "3.2.0") + @Deprecated public Rectangle setHorizontalGradient(int colorLeft, int colorRight) { - return setColor(colorLeft, colorRight, colorLeft, colorRight); + return horizontalGradient(colorLeft, colorRight); + } + + public Rectangle horizontalGradient(int colorLeft, int colorRight) { + return color(colorLeft, colorRight, colorLeft, colorRight); } + @ApiStatus.ScheduledForRemoval(inVersion = "3.2.0") + @Deprecated public Rectangle setColor(int color) { - return setColor(color, color, color, color); + return color(color); } + public Rectangle color(int color) { + return color(color, color, color, color); + } + + @ApiStatus.ScheduledForRemoval(inVersion = "3.2.0") + @Deprecated public Rectangle setCornerSegments(int cornerSegments) { + return cornerSegments(cornerSegments); + } + + public Rectangle cornerSegments(int cornerSegments) { this.cornerSegments = cornerSegments; return this; } + @ApiStatus.ScheduledForRemoval(inVersion = "3.2.0") + @Deprecated public Rectangle setCanApplyTheme(boolean canApplyTheme) { + return canApplyTheme(canApplyTheme); + } + + public Rectangle canApplyTheme(boolean canApplyTheme) { this.canApplyTheme = canApplyTheme; return this; } + public Rectangle solid() { + this.borderThickness = 0; + return this; + } + + public Rectangle hollow(float borderThickness) { + this.borderThickness = borderThickness; + if (borderThickness > 0 && this.cornerRadius > 0) { + ModularUI.LOGGER.error("Hollow rectangles currently can't have a corner radius."); + } + return this; + } + + public Rectangle hollow() { + return hollow(1); + } + @SideOnly(Side.CLIENT) @Override public void draw(GuiContext context, int x0, int y0, int width, int height, WidgetTheme widgetTheme) { applyColor(widgetTheme.getColor()); - if (this.cornerRadius <= 0) { - GuiDraw.drawRect(x0, y0, width, height, this.colorTL, this.colorTR, this.colorBL, this.colorBR); - return; + if (this.borderThickness <= 0) { + if (this.cornerRadius <= 0) { + GuiDraw.drawRect(x0, y0, width, height, this.colorTL, this.colorTR, this.colorBL, this.colorBR); + return; + } + GuiDraw.drawRoundedRect(x0, y0, width, height, this.colorTL, this.colorTR, this.colorBL, this.colorBR, this.cornerRadius, this.cornerSegments); + } else { + float d = this.borderThickness; + float x1 = x0 + width, y1 = y0 + height; + Platform.setupDrawColor(); + Platform.setupDrawGradient(); + Platform.startDrawing(Platform.DrawMode.TRIANGLE_STRIP, Platform.VertexFormat.POS_COLOR, buffer -> { + v(buffer, x0, y0, this.colorTL); + v(buffer, x1 - d, y0 + d, this.colorTR); + v(buffer, x1, y0, this.colorTR); + v(buffer, x1 - d, y1 - d, this.colorBR); + v(buffer, x1, y1, this.colorBR); + v(buffer, x0 + d, y1 - d, this.colorBL); + v(buffer, x0, y1, this.colorBL); + v(buffer, x0 + d, y0 + d, this.colorTL); + v(buffer, x0, y0, this.colorTL); + v(buffer, x1 - d, y0 + d, this.colorTR); + }); + Platform.endDrawGradient(); } - GuiDraw.drawRoundedRect(x0, y0, width, height, this.colorTL, this.colorTR, this.colorBL, this.colorBR, this.cornerRadius, this.cornerSegments); + } + + private static void v(BufferBuilder buffer, float x, float y, int c) { + buffer.pos(x, y, 0).color(Color.getRed(c), Color.getGreen(c), Color.getBlue(c), Color.getAlpha(c)).endVertex(); } @Override @@ -88,7 +176,7 @@ public boolean canApplyTheme() { @Override public void loadFromJson(JsonObject json) { if (json.has("color")) { - setColor(Color.ofJson(json.get("color"))); + color(Color.ofJson(json.get("color"))); } if (json.has("colorTop")) { int c = Color.ofJson(json.get("colorTop")); @@ -116,6 +204,13 @@ public void loadFromJson(JsonObject json) { setColor(json, val -> this.colorBR = val, "colorBottomRight", "colorBR"); this.cornerRadius = JsonHelper.getInt(json, 0, "cornerRadius"); this.cornerSegments = JsonHelper.getInt(json, 10, "cornerSegments"); + if (JsonHelper.getBoolean(json, false, "solid")) { + this.borderThickness = 0; + } else if (JsonHelper.getBoolean(json, false, "hollow")) { + this.borderThickness = 1; + } else { + this.borderThickness = JsonHelper.getFloat(json, 0, "borderThickness"); + } } @Override @@ -126,6 +221,7 @@ public boolean saveToJson(JsonObject json) { json.addProperty("colorBR", this.colorBR); json.addProperty("cornerRadius", this.cornerRadius); json.addProperty("cornerSegments", this.cornerSegments); + json.addProperty("borderThickness", this.borderThickness); return true; } @@ -140,19 +236,19 @@ private void setColor(JsonObject json, IntConsumer color, String... keys) { public Rectangle interpolate(Rectangle start, Rectangle end, float t) { this.cornerRadius = Interpolations.lerp(start.cornerRadius, end.cornerRadius, t); this.cornerSegments = Interpolations.lerp(start.cornerSegments, end.cornerSegments, t); - this.colorTL = Color.interpolate(start.colorTL, end.colorTL, t); - this.colorTR = Color.interpolate(start.colorTR, end.colorTR, t); - this.colorBL = Color.interpolate(start.colorBL, end.colorBL, t); - this.colorBR = Color.interpolate(start.colorBR, end.colorBR, t); + this.colorTL = Color.lerp(start.colorTL, end.colorTL, t); + this.colorTR = Color.lerp(start.colorTR, end.colorTR, t); + this.colorBL = Color.lerp(start.colorBL, end.colorBL, t); + this.colorBR = Color.lerp(start.colorBR, end.colorBR, t); return this; } @Override public Rectangle copyOrImmutable() { return new Rectangle() - .setColor(this.colorTL, this.colorTR, this.colorBL, this.colorBR) - .setCornerRadius(this.cornerRadius) - .setCornerSegments(this.cornerSegments) - .setCanApplyTheme(this.canApplyTheme); + .color(this.colorTL, this.colorTR, this.colorBL, this.colorBR) + .cornerRadius(this.cornerRadius) + .cornerSegments(this.cornerSegments) + .canApplyTheme(this.canApplyTheme); } } diff --git a/src/main/java/com/cleanroommc/modularui/test/GLTestGui.java b/src/main/java/com/cleanroommc/modularui/test/GLTestGui.java index d98a174d5..fcb2f4739 100644 --- a/src/main/java/com/cleanroommc/modularui/test/GLTestGui.java +++ b/src/main/java/com/cleanroommc/modularui/test/GLTestGui.java @@ -61,7 +61,7 @@ public class GLTestGui extends CustomModularScreen { .coverChildrenHeight() .child(buildRenderObjectConfig(this.ro1) .name("config left col")) - .child(new Rectangle().setColor(Color.TEXT_COLOR_DARK).asWidget() + .child(new Rectangle().color(Color.TEXT_COLOR_DARK).asWidget() .name("separator") .width(1) .margin(2, 0) diff --git a/src/main/java/com/cleanroommc/modularui/test/TestGuis.java b/src/main/java/com/cleanroommc/modularui/test/TestGuis.java index 6d7247b51..0b75f85a2 100644 --- a/src/main/java/com/cleanroommc/modularui/test/TestGuis.java +++ b/src/main/java/com/cleanroommc/modularui/test/TestGuis.java @@ -186,7 +186,7 @@ public void onInit() { public static @NotNull ModularPanel buildPostTheLogAnimationUI() { Animator post = new Animator().curve(Interpolation.SINE_IN).duration(300).bounds(-35, 0); Animator the = new Animator().curve(Interpolation.SINE_IN).duration(300).bounds(-20, 0); - Animator extraordinary = new Animator().curve(Interpolation.SINE_IN).duration(500).bounds(53, 0); + Animator extraordinary = new Animator().curve(Interpolation.SINE_IN).duration(300).bounds(53, 0); Animator log = new Animator().curve(Interpolation.SINE_IN).duration(300).bounds(20, 0); Animator logGrow = new Animator().curve(Interpolation.LINEAR).duration(2500).bounds(0f, 1f); IAnimator animator = new Wait(300) @@ -232,6 +232,10 @@ public void onInit() { float period = 3000f; return ModularPanel.defaultPanel("main") .size(150) + .overlay(new Rectangle() + .color(Color.GREEN.main) + .hollow(2) + .asIcon().margin(5)) .child(new TextWidget<>(IKey.str("Test String")).scale(0.6f).horizontalCenter().top(7)) .child(new DraggableWidget<>() //.background(new SpriteDrawable(sprite)) @@ -432,8 +436,8 @@ public static ModularPanel buildCollapseDisabledChildrenUI() { } }; - Rectangle color1 = new Rectangle().setColor(Color.BLACK.main); - Rectangle color2 = new Rectangle().setColor(Color.WHITE.main); + Rectangle color1 = new Rectangle().color(Color.BLACK.main); + Rectangle color2 = new Rectangle().color(Color.WHITE.main); IDrawable gradient = (context1, x, y, width, height, widgetTheme) -> GuiDraw.drawHorizontalGradientRect(x, y, width, height, color1.getColor(), color2.getColor()); IDrawable correctedGradient = (context1, x, y, width, height, widgetTheme) -> { @@ -456,12 +460,12 @@ public static ModularPanel buildCollapseDisabledChildrenUI() { ModularPanel panel = new ModularPanel("colors").width(300).coverChildrenHeight().padding(7); - IPanelHandler colorPicker1 = IPanelHandler.simple(panel, (mainPanel, player) -> new ColorPickerDialog("color_picker1", color1::setColor, color1.getColor(), true) + IPanelHandler colorPicker1 = IPanelHandler.simple(panel, (mainPanel, player) -> new ColorPickerDialog("color_picker1", color1::color, color1.getColor(), true) .setDraggable(true) .relative(panel) .top(0) .rightRel(1f), true); - IPanelHandler colorPicker2 = IPanelHandler.simple(panel, (mainPanel, player) -> new ColorPickerDialog("color_picker2", color2::setColor, color2.getColor(), true) + IPanelHandler colorPicker2 = IPanelHandler.simple(panel, (mainPanel, player) -> new ColorPickerDialog("color_picker2", color2::color, color2.getColor(), true) .setDraggable(true) .relative(panel) .top(0) diff --git a/src/main/java/com/cleanroommc/modularui/test/TestTile.java b/src/main/java/com/cleanroommc/modularui/test/TestTile.java index b440a5d97..4956eb490 100644 --- a/src/main/java/com/cleanroommc/modularui/test/TestTile.java +++ b/src/main/java/com/cleanroommc/modularui/test/TestTile.java @@ -173,10 +173,10 @@ public ModularPanel buildUI(PosGuiData guiData, PanelSyncManager syncManager, UI return flow; }); - Rectangle colorPickerBackground = new Rectangle().setColor(Color.RED.main); + Rectangle colorPickerBackground = new Rectangle().color(Color.RED.main); ModularPanel panel = new ModularPanel("test_tile"); IPanelHandler panelSyncHandler = syncManager.syncedPanel("other_panel", true, this::openSecondWindow); - IPanelHandler colorPicker = IPanelHandler.simple(panel, (mainPanel, player) -> new ColorPickerDialog(colorPickerBackground::setColor, colorPickerBackground.getColor(), true) + IPanelHandler colorPicker = IPanelHandler.simple(panel, (mainPanel, player) -> new ColorPickerDialog(colorPickerBackground::color, colorPickerBackground.getColor(), true) .setDraggable(true) .relative(panel) .top(0) @@ -443,7 +443,7 @@ public ModularPanel buildUI(PosGuiData guiData, PanelSyncManager syncManager, UI .disableHoverBackground() .setNumbers(1, Short.MAX_VALUE) .setTextAlignment(Alignment.Center) - .background(new Rectangle().setColor(0xFFb1b1b1)) + .background(new Rectangle().color(0xFFb1b1b1)) .setTextColor(IKey.TEXT_COLOR) .size(20, 14)) .child(IKey.str("Number config").asWidget() diff --git a/src/main/java/com/cleanroommc/modularui/widgets/ColorPickerDialog.java b/src/main/java/com/cleanroommc/modularui/widgets/ColorPickerDialog.java index 304961628..e49056204 100644 --- a/src/main/java/com/cleanroommc/modularui/widgets/ColorPickerDialog.java +++ b/src/main/java/com/cleanroommc/modularui/widgets/ColorPickerDialog.java @@ -20,7 +20,7 @@ public class ColorPickerDialog extends Dialog { - private static final IDrawable handleBackground = new Rectangle().setColor(Color.WHITE.main); + private static final IDrawable handleBackground = new Rectangle().color(Color.WHITE.main); private int color; private int red; @@ -262,12 +262,12 @@ public void updateColor(int color) { int gs = Color.withGreen(color, 0), ge = Color.withGreen(color, 255); int bs = Color.withBlue(color, 0), be = Color.withBlue(color, 255); int as = Color.withAlpha(color, 0), ae = Color.withAlpha(color, 255); - this.sliderBackgroundR.setHorizontalGradient(rs, re); - this.sliderBackgroundG.setHorizontalGradient(gs, ge); - this.sliderBackgroundB.setHorizontalGradient(bs, be); - this.sliderBackgroundA.setHorizontalGradient(as, ae); - this.sliderBackgroundS.setHorizontalGradient(Color.withHSVSaturation(color, 0f), Color.withHSVSaturation(color, 1f)); - this.sliderBackgroundV.setHorizontalGradient(Color.withValue(color, 0f), Color.withValue(color, 1f)); - this.preview.setColor(color); + this.sliderBackgroundR.horizontalGradient(rs, re); + this.sliderBackgroundG.horizontalGradient(gs, ge); + this.sliderBackgroundB.horizontalGradient(bs, be); + this.sliderBackgroundA.horizontalGradient(as, ae); + this.sliderBackgroundS.horizontalGradient(Color.withHSVSaturation(color, 0f), Color.withHSVSaturation(color, 1f)); + this.sliderBackgroundV.horizontalGradient(Color.withValue(color, 0f), Color.withValue(color, 1f)); + this.preview.color(color); } } diff --git a/src/main/java/com/cleanroommc/modularui/widgets/SliderWidget.java b/src/main/java/com/cleanroommc/modularui/widgets/SliderWidget.java index cc501b0eb..9ee469d4d 100644 --- a/src/main/java/com/cleanroommc/modularui/widgets/SliderWidget.java +++ b/src/main/java/com/cleanroommc/modularui/widgets/SliderWidget.java @@ -25,7 +25,7 @@ public class SliderWidget extends Widget implements Interactable { private IDoubleValue doubleValue; - private IDrawable stopperDrawable = new Rectangle().setColor(Color.withAlpha(Color.WHITE.main, 0.4f)); + private IDrawable stopperDrawable = new Rectangle().color(Color.withAlpha(Color.WHITE.main, 0.4f)); private IDrawable handleDrawable = GuiTextures.BUTTON_CLEAN; private GuiAxis axis = GuiAxis.X; private DoubleList stopper; From 566b87981f36c42c7babb62e39c305086a50e3f1 Mon Sep 17 00:00:00 2001 From: brachy84 Date: Mon, 8 Dec 2025 21:37:57 +0100 Subject: [PATCH 18/50] experimental graph plotting (cherry picked from commit 86a01ef83bfe163b77747576cff0cfaa3934a601) --- .../modularui/drawable/GuiDraw.java | 119 +++++-- .../drawable/graph/AutoMajorTickFinder.java | 30 ++ .../drawable/graph/AutoMinorTickFinder.java | 35 +++ .../modularui/drawable/graph/GraphAxis.java | 190 +++++++++++ .../drawable/graph/GraphDrawable.java | 247 +++++++++++++++ .../modularui/drawable/graph/GraphView.java | 57 ++++ .../drawable/graph/MajorTickFinder.java | 9 + .../drawable/graph/MinorTickFinder.java | 9 + .../modularui/screen/ClientScreenHandler.java | 4 +- .../cleanroommc/modularui/test/TestGuis.java | 33 +- .../modularui/utils/FloatArrayMath.java | 294 ++++++++++++++++++ 11 files changed, 1005 insertions(+), 22 deletions(-) create mode 100644 src/main/java/com/cleanroommc/modularui/drawable/graph/AutoMajorTickFinder.java create mode 100644 src/main/java/com/cleanroommc/modularui/drawable/graph/AutoMinorTickFinder.java create mode 100644 src/main/java/com/cleanroommc/modularui/drawable/graph/GraphAxis.java create mode 100644 src/main/java/com/cleanroommc/modularui/drawable/graph/GraphDrawable.java create mode 100644 src/main/java/com/cleanroommc/modularui/drawable/graph/GraphView.java create mode 100644 src/main/java/com/cleanroommc/modularui/drawable/graph/MajorTickFinder.java create mode 100644 src/main/java/com/cleanroommc/modularui/drawable/graph/MinorTickFinder.java create mode 100644 src/main/java/com/cleanroommc/modularui/utils/FloatArrayMath.java diff --git a/src/main/java/com/cleanroommc/modularui/drawable/GuiDraw.java b/src/main/java/com/cleanroommc/modularui/drawable/GuiDraw.java index 417fad473..1f455168f 100644 --- a/src/main/java/com/cleanroommc/modularui/drawable/GuiDraw.java +++ b/src/main/java/com/cleanroommc/modularui/drawable/GuiDraw.java @@ -32,6 +32,7 @@ import cpw.mods.fml.relauncher.SideOnly; import com.mitchej123.hodgepodge.textures.IPatchedTextureAtlasSprite; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.Nullable; import org.lwjgl.opengl.GL11; @@ -47,16 +48,8 @@ public class GuiDraw { public static void drawRect(float x0, float y0, float w, float h, int color) { Platform.setupDrawColor(); - float x1 = x0 + w, y1 = y0 + h; - float r = Color.getRedF(color); - float g = Color.getGreenF(color); - float b = Color.getBlueF(color); - float a = Color.getAlphaF(color); Platform.startDrawing(Platform.DrawMode.QUADS, Platform.VertexFormat.POS_COLOR, bufferBuilder -> { - bufferBuilder.pos(x0, y0, 0.0f).color(r, g, b, a).endVertex(); - bufferBuilder.pos(x0, y1, 0.0f).color(r, g, b, a).endVertex(); - bufferBuilder.pos(x1, y1, 0.0f).color(r, g, b, a).endVertex(); - bufferBuilder.pos(x1, y0, 0.0f).color(r, g, b, a).endVertex(); + drawRectRaw(bufferBuilder, x0, y0, x0 + w, y0 + h, color); }); } @@ -81,6 +74,21 @@ public static void drawRect(float x0, float y0, float w, float h, int colorTL, i Platform.endDrawGradient(); } + public static void drawRectRaw(BufferBuilder buffer, float x0, float y0, float x1, float y1, int color) { + int r = Color.getRed(color); + int g = Color.getGreen(color); + int b = Color.getBlue(color); + int a = Color.getAlpha(color); + drawRectRaw(buffer, x0, y0, x1, y1, r, g, b, a); + } + + public static void drawRectRaw(BufferBuilder buffer, float x0, float y0, float x1, float y1, int r, int g, int b, int a) { + buffer.pos(x0, y0, 0.0f).color(r, g, b, a).endVertex(); + buffer.pos(x0, y1, 0.0f).color(r, g, b, a).endVertex(); + buffer.pos(x1, y1, 0.0f).color(r, g, b, a).endVertex(); + buffer.pos(x1, y0, 0.0f).color(r, g, b, a).endVertex(); + } + public static void drawCircle(float x0, float y0, float diameter, int color, int segments) { drawEllipse(x0, y0, diameter, diameter, color, color, segments); } @@ -415,26 +423,102 @@ public static void drawTiledSprite(TextureMap textureMap, TextureAtlasSprite spr GlStateManager.enableAlpha(); }*/ + /** + * @deprecated no replacement + */ + @ApiStatus.ScheduledForRemoval(inVersion = "3.2.0") + @Deprecated public static void drawOutlineCenter(int x, int y, int offset, int color) { drawOutlineCenter(x, y, offset, color, 1); } + /** + * @deprecated no replacement + */ + @ApiStatus.ScheduledForRemoval(inVersion = "3.2.0") + @Deprecated public static void drawOutlineCenter(int x, int y, int offset, int color, int border) { drawOutline(x - offset, y - offset, x + offset, y + offset, color, border); } + @ApiStatus.ScheduledForRemoval(inVersion = "3.2.0") + @Deprecated public static void drawOutline(int left, int top, int right, int bottom, int color) { drawOutline(left, top, right, bottom, color, 1); } /** * Draw rectangle outline with given border + * + * @deprecated use {@link #drawBorderInsideLTRB(float, float, float, float, float, int)} or {@link #drawBorderInsideLTRB(float, float, float, float, float, int)} */ + @ApiStatus.ScheduledForRemoval(inVersion = "3.2.0") + @Deprecated public static void drawOutline(int left, int top, int right, int bottom, int color, int border) { - Gui.drawRect(left, top, left + border, bottom, color); - Gui.drawRect(right - border, top, right, bottom, color); - Gui.drawRect(left + border, top, right - border, top + border, color); - Gui.drawRect(left + border, bottom - border, right - border, bottom, color); + drawBorderInsideLTRB(left, top, right, bottom, border, color); + } + + private static void drawBorderLTRB(float left, float top, float right, float bottom, float border, int color, boolean outside) { + if (outside) { + left -= border; + top -= border; + right += border; + bottom += border; + } + float x0 = left, y0 = top, x1 = right, y1 = bottom, d = border; + Platform.setupDrawColor(); + Platform.startDrawing(Platform.DrawMode.TRIANGLE_STRIP, Platform.VertexFormat.POS_COLOR, buffer -> { + pc(buffer, x0, y0, color); + pc(buffer, x1 - d, y0 + d, color); + pc(buffer, x1, y0, color); + pc(buffer, x1 - d, y1 - d, color); + pc(buffer, x1, y1, color); + pc(buffer, x0 + d, y1 - d, color); + pc(buffer, x0, y1, color); + pc(buffer, x0 + d, y0 + d, color); + pc(buffer, x0, y0, color); + pc(buffer, x1 - d, y0 + d, color); + }); + } + + public static void drawBorderOutsideLTRB(float left, float top, float right, float bottom, int color) { + drawBorderLTRB(left, top, right, bottom, 1, color, true); + } + + public static void drawBorderOutsideLTRB(float left, float top, float right, float bottom, float border, int color) { + drawBorderLTRB(left, top, right, bottom, border, color, true); + } + + public static void drawBorderInsideLTRB(float left, float top, float right, float bottom, int color) { + drawBorderLTRB(left, top, right, bottom, 1, color, false); + } + + public static void drawBorderInsideLTRB(float left, float top, float right, float bottom, float border, int color) { + drawBorderLTRB(left, top, right, bottom, border, color, false); + } + + private static void drawBorderXYWH(float x, float y, float w, float h, float border, int color, boolean outside) { + drawBorderLTRB(x, y, x + w, y + h, border, color, outside); + } + + public static void drawBorderOutsideXYWH(float x, float y, float w, float h, float border, int color) { + drawBorderXYWH(x, y, w, h, border, color, true); + } + + public static void drawBorderOutsideXYWH(float x, float y, float w, float h, int color) { + drawBorderXYWH(x, y, w, h, 1, color, true); + } + + public static void drawBorderInsideXYWH(float x, float y, float w, float h, float border, int color) { + drawBorderXYWH(x, y, w, h, border, color, false); + } + + public static void drawBorderInsideXYWH(float x, float y, float w, float h, int color) { + drawBorderXYWH(x, y, w, h, 1, color, false); + } + + private static void pc(BufferBuilder buffer, float x, float y, int c) { + buffer.pos(x, y, 0).color(Color.getRed(c), Color.getGreen(c), Color.getBlue(c), Color.getAlpha(c)).endVertex(); } /** @@ -559,15 +643,12 @@ public static void drawDropCircleShadow(int x, int y, int radius, int offset, in Platform.endDrawGradient(); } - @SideOnly(Side.CLIENT) + @ApiStatus.ScheduledForRemoval(inVersion = "3.2.0") + @Deprecated public static void drawBorder(float x, float y, float width, float height, int color, float border) { - drawRect(x - border, y - border, width + 2 * border, border, color); - drawRect(x - border, y + height, width + 2 * border, border, color); - drawRect(x - border, y, border, height, color); - drawRect(x + width, y, border, height, color); + drawBorderLTRB(x, y, x + width, y + height, border, color, false); } - @SideOnly(Side.CLIENT) public static void drawText(String text, float x, float y, float scale, int color, boolean shadow) { FontRenderer fontRenderer = Minecraft.getMinecraft().fontRenderer; Platform.setupDrawFont(); diff --git a/src/main/java/com/cleanroommc/modularui/drawable/graph/AutoMajorTickFinder.java b/src/main/java/com/cleanroommc/modularui/drawable/graph/AutoMajorTickFinder.java new file mode 100644 index 000000000..0e5fb6361 --- /dev/null +++ b/src/main/java/com/cleanroommc/modularui/drawable/graph/AutoMajorTickFinder.java @@ -0,0 +1,30 @@ +package com.cleanroommc.modularui.drawable.graph; + +import org.jetbrains.annotations.ApiStatus; + +@ApiStatus.Experimental +public class AutoMajorTickFinder implements MajorTickFinder { + + private float multiple; + + public AutoMajorTickFinder(float multiple) { + this.multiple = multiple; + } + + @Override + public float[] find(float min, float max, float[] ticks) { + int s = (int) Math.ceil((max - min) / multiple) + 1; + if (s > ticks.length) ticks = new float[s]; + float next = (float) (Math.floor(min / multiple) * multiple); + for (int i = 0; i < s; i++) { + if (next > max) { + s--; + break; + } + ticks[i] = next; + next += multiple; + } + if (ticks.length > s) ticks[s] = Float.NaN; + return ticks; + } +} diff --git a/src/main/java/com/cleanroommc/modularui/drawable/graph/AutoMinorTickFinder.java b/src/main/java/com/cleanroommc/modularui/drawable/graph/AutoMinorTickFinder.java new file mode 100644 index 000000000..c1c678adf --- /dev/null +++ b/src/main/java/com/cleanroommc/modularui/drawable/graph/AutoMinorTickFinder.java @@ -0,0 +1,35 @@ +package com.cleanroommc.modularui.drawable.graph; + +import org.jetbrains.annotations.ApiStatus; + +@ApiStatus.Experimental +public class AutoMinorTickFinder implements MinorTickFinder { + + private int amountBetweenMajors; + + public AutoMinorTickFinder(int amountBetweenMajors) { + this.amountBetweenMajors = amountBetweenMajors; + } + + @Override + public float[] find(float min, float max, float[] majorTicks, float[] ticks) { + int s = majorTicks.length * this.amountBetweenMajors; + if (ticks.length < s) ticks = new float[s]; + int k = 0; + for (int i = 0; i < majorTicks.length - 1; i++) { + if (Float.isNaN(majorTicks[i + 1])) break; + float next = majorTicks[i]; + float d = (majorTicks[i + 1] - next) / (amountBetweenMajors + 1); + for (int j = 0; j < amountBetweenMajors; j++) { + next += d; + if (next >= min) ticks[k++] = next; + if (next > max) { + ticks[k] = Float.NaN; + break; + } + } + } + if (k < ticks.length) ticks[k] = Float.NaN; + return ticks; + } +} diff --git a/src/main/java/com/cleanroommc/modularui/drawable/graph/GraphAxis.java b/src/main/java/com/cleanroommc/modularui/drawable/graph/GraphAxis.java new file mode 100644 index 000000000..9da775a1c --- /dev/null +++ b/src/main/java/com/cleanroommc/modularui/drawable/graph/GraphAxis.java @@ -0,0 +1,190 @@ +package com.cleanroommc.modularui.drawable.graph; + +import com.cleanroommc.modularui.api.GuiAxis; +import com.cleanroommc.modularui.drawable.GuiDraw; +import com.cleanroommc.modularui.drawable.text.TextRenderer; +import com.cleanroommc.modularui.utils.Alignment; +import com.cleanroommc.modularui.utils.FloatArrayMath; + +import net.minecraft.client.renderer.BufferBuilder; + +import org.jetbrains.annotations.ApiStatus; + +import java.text.DecimalFormat; + +@ApiStatus.Experimental +public class GraphAxis { + + private static final TextRenderer textRenderer = new TextRenderer(); + private static final float TICK_LABEL_SCALE = 0.4f; + private static final float AXIS_LABEL_SCALE = 1f; + private static final float TICK_LABEL_OFFSET = 2f; + private static final float AXIS_LABEL_OFFSET = 3f; + + public final GuiAxis axis; + + public float[] majorTicks = new float[8]; + public float[] minorTicks = new float[16]; + public TextRenderer.Line[] tickLabels = new TextRenderer.Line[8]; + private float maxLabelWidth = 0; + public MajorTickFinder majorTickFinder = new AutoMajorTickFinder(10); + public MinorTickFinder minorTickFinder = new AutoMinorTickFinder(1); + public String label; + public float min, max; + public boolean autoLimits = true; + public float[] data; + + public GraphAxis(GuiAxis axis) { + this.axis = axis; + } + + void compute() { + if (this.autoLimits) { + this.min = FloatArrayMath.min(this.data); + this.max = FloatArrayMath.max(this.data); + } + this.majorTicks = this.majorTickFinder.find(this.min, this.max, this.majorTicks); + this.minorTicks = this.minorTickFinder.find(this.min, this.max, this.majorTicks, this.minorTicks); + + if (this.tickLabels.length < this.majorTicks.length) { + this.tickLabels = new TextRenderer.Line[this.majorTicks.length]; + } + textRenderer.setScale(TICK_LABEL_SCALE); + this.maxLabelWidth = 0; + float maxDiff = FloatArrayMath.max(FloatArrayMath.diff(this.majorTicks)); + int significantPlaces = (int) Math.abs(Math.log10(maxDiff)) + 1; + DecimalFormat format = new DecimalFormat(); + format.setMaximumFractionDigits(significantPlaces); + for (int i = 0; i < this.tickLabels.length; i++) { + if (Float.isNaN(this.majorTicks[i])) break; + this.tickLabels[i] = textRenderer.line(format.format(this.majorTicks[i])); + if (this.tickLabels[i].getWidth() > this.maxLabelWidth) { + this.maxLabelWidth = this.tickLabels[i].getWidth(); + } + } + } + + void applyPadding(GraphView graphView) { + textRenderer.setScale(TICK_LABEL_SCALE); + if (this.axis.isHorizontal()) { + graphView.sy1 -= textRenderer.getFontHeight() + TICK_LABEL_OFFSET; + if (this.label != null) { + textRenderer.setScale(AXIS_LABEL_SCALE); + graphView.sy1 -= textRenderer.getFontHeight() + AXIS_LABEL_OFFSET; + } + } else { + float off = this.maxLabelWidth + TICK_LABEL_OFFSET; + if (this.label != null) { + textRenderer.setScale(AXIS_LABEL_SCALE); + off += textRenderer.getFontHeight() + AXIS_LABEL_OFFSET; + } + graphView.sx0 += off; + graphView.sx1 -= off; + } + } + + void drawGridLines(BufferBuilder buffer, GraphView view, GraphAxis other, boolean major, float d, int r, int g, int b, int a) { + float[] pos = major ? this.majorTicks : this.minorTicks; + float dHalf = d / 2; + if (axis.isHorizontal()) { + float otherMin = view.transformYGraphToScreen(other.max); + float otherMax = view.transformYGraphToScreen(other.min); + drawLinesOnHorizontal(buffer, view, pos, dHalf, otherMin, otherMax, r, g, b, a); + } else { + float otherMin = view.transformXGraphToScreen(other.min); + float otherMax = view.transformXGraphToScreen(other.max); + drawLinesOnVertical(buffer, view, pos, dHalf, otherMin, otherMax, r, g, b, a); + } + } + + void drawTicks(BufferBuilder buffer, GraphView view, GraphAxis other, boolean major, float thickness, float length, int r, int g, int b, int a) { + float[] pos = major ? this.majorTicks : this.minorTicks; + float dHalf = thickness / 2; + if (axis.isHorizontal()) { + float otherMin = view.transformYGraphToScreen(other.min); + drawLinesOnHorizontal(buffer, view, pos, dHalf, otherMin - length, otherMin, r, g, b, a); + } else { + float otherMin = view.transformXGraphToScreen(other.min); + drawLinesOnVertical(buffer, view, pos, dHalf, otherMin, otherMin + length, r, g, b, a); + } + } + + private void drawLinesOnHorizontal(BufferBuilder buffer, GraphView view, float[] pos, float dHalf, + float crossLow, float crossHigh, int r, int g, int b, int a) { + for (float p : pos) { + if (Float.isNaN(p)) break; + if (p < min || p > max) continue; + + p = view.transformXGraphToScreen(p); + + float x0 = p - dHalf; + float x1 = p + dHalf; + GuiDraw.drawRectRaw(buffer, x0, crossLow, x1, crossHigh, r, g, b, a); + } + } + + private void drawLinesOnVertical(BufferBuilder buffer, GraphView view, float[] pos, float dHalf, + float crossLow, float crossHigh, int r, int g, int b, int a) { + for (float p : pos) { + if (Float.isNaN(p)) break; + if (p < min || p > max) continue; + + p = view.transformYGraphToScreen(p); + + float y0 = p - dHalf; + float y1 = p + dHalf; + GuiDraw.drawRectRaw(buffer, crossLow, y0, crossHigh, y1, r, g, b, a); + } + } + + void drawLabels(GraphView view, GraphAxis other) { + textRenderer.setHardWrapOnBorder(false); + if (axis.isHorizontal()) { + textRenderer.setScale(TICK_LABEL_SCALE); + textRenderer.setAlignment(Alignment.TopCenter, 100); + float y = view.transformYGraphToScreen(other.min) + TICK_LABEL_OFFSET; + for (int i = 0; i < this.majorTicks.length; i++) { + float pos = this.majorTicks[i]; + if (Float.isNaN(pos)) break; + if (pos < min || pos > max) continue; + textRenderer.setPos((int) (view.transformXGraphToScreen(pos) - 50), (int) y); + textRenderer.draw(this.tickLabels[i].getText()); + } + } else { + textRenderer.setScale(TICK_LABEL_SCALE); + textRenderer.setAlignment(Alignment.CenterRight, this.maxLabelWidth, 20); + float x = view.transformXGraphToScreen(other.min) - TICK_LABEL_OFFSET - this.maxLabelWidth; + for (int i = 0; i < this.majorTicks.length; i++) { + float pos = this.majorTicks[i]; + if (Float.isNaN(pos)) break; + if (pos < min || pos > max) continue; + textRenderer.setPos((int) x, (int) (view.transformYGraphToScreen(pos) - 10)); + textRenderer.draw(this.tickLabels[i].getText()); + } + } + } + + public GuiAxis getAxis() { + return axis; + } + + public float getMax() { + return max; + } + + public float getMin() { + return min; + } + + public MajorTickFinder getMajorTickFinder() { + return majorTickFinder; + } + + public MinorTickFinder getMinorTickFinder() { + return minorTickFinder; + } + + public String getLabel() { + return label; + } +} diff --git a/src/main/java/com/cleanroommc/modularui/drawable/graph/GraphDrawable.java b/src/main/java/com/cleanroommc/modularui/drawable/graph/GraphDrawable.java new file mode 100644 index 000000000..9966fad81 --- /dev/null +++ b/src/main/java/com/cleanroommc/modularui/drawable/graph/GraphDrawable.java @@ -0,0 +1,247 @@ +package com.cleanroommc.modularui.drawable.graph; + +import com.cleanroommc.modularui.api.GuiAxis; +import com.cleanroommc.modularui.api.drawable.IDrawable; +import com.cleanroommc.modularui.drawable.GuiDraw; +import com.cleanroommc.modularui.screen.viewport.GuiContext; +import com.cleanroommc.modularui.theme.WidgetTheme; +import com.cleanroommc.modularui.utils.Color; +import com.cleanroommc.modularui.utils.MathUtils; +import com.cleanroommc.modularui.utils.Platform; + +import net.minecraft.client.renderer.BufferBuilder; + +import org.jetbrains.annotations.ApiStatus; + +@ApiStatus.Experimental +public class GraphDrawable implements IDrawable { + + private final GraphView view = new GraphView(); + //private IDrawable background; + private int backgroundColor = Color.WHITE.main; + + private float lineWidth = 2; + private int lineColor = Color.BLUE_ACCENT.main; + + private boolean borderTop = true, borderLeft = true, borderBottom = false, borderRight = false; + + private float gridLineWidth = 0.5f; + private int gridLineColor = Color.withAlpha(Color.BLACK.main, 0.4f); + private float minorGridLineWidth = 0.25f; + private int minorGridLineColor = Color.withAlpha(Color.BLACK.main, 0.15f); + + private final GraphAxis x = new GraphAxis(GuiAxis.X), y = new GraphAxis(GuiAxis.Y); + + private float majorTickThickness = 0.5f, majorTickLength = 1f, minorTickThickness = 0.25f, minorTickLength = 0.5f; + + private boolean dirty = true; + + public void redraw() { + this.dirty = true; + } + + @Override + public void draw(GuiContext context, int x, int y, int width, int height, WidgetTheme widgetTheme) { + if (this.view.setScreen(x, y, x + width, y + height) | compute()) { // we always want compute() to be called, + this.x.applyPadding(this.view); + this.y.applyPadding(this.view); + this.view.setGraph(this.x.min, this.y.min, this.x.max, this.y.max); + } + + if (this.backgroundColor != 0) { + GuiDraw.drawRect(this.view.sx0, this.view.sy0, this.view.sx1 - this.view.sx0, this.view.sy1 - this.view.sy0, this.backgroundColor); + } + + Platform.setupDrawColor(); + Platform.startDrawing(Platform.DrawMode.QUADS, Platform.VertexFormat.POS_COLOR, buffer -> { + drawGrid(context, buffer); + }); + drawData(context, this.view, this.x.data, this.y.data, this.lineWidth, this.lineColor); + + GuiDraw.drawBorderOutsideLTRB(this.view.sx0, this.view.sy0, this.view.sx1, this.view.sy1, 0.5f, Color.BLACK.main); + this.x.drawLabels(this.view, this.y); + this.y.drawLabels(this.view, this.x); + } + + public void drawGrid(GuiContext context, BufferBuilder buffer) { + if (this.minorGridLineWidth > 0) { + int r = Color.getRed(this.minorGridLineColor); + int g = Color.getGreen(this.minorGridLineColor); + int b = Color.getBlue(this.minorGridLineColor); + int a = Color.getAlpha(this.minorGridLineColor); + this.x.drawGridLines(buffer, this.view, this.y, false, this.minorGridLineWidth, r, g, b, a); + this.y.drawGridLines(buffer, this.view, this.x, false, this.minorGridLineWidth, r, g, b, a); + } + if (this.gridLineWidth > 0) { + int r = Color.getRed(this.gridLineColor); + int g = Color.getGreen(this.gridLineColor); + int b = Color.getBlue(this.gridLineColor); + int a = Color.getAlpha(this.gridLineColor); + this.x.drawGridLines(buffer, this.view, this.y, true, this.gridLineWidth, r, g, b, a); + this.y.drawGridLines(buffer, this.view, this.x, true, this.gridLineWidth, r, g, b, a); + } + + this.x.drawTicks(buffer, this.view, this.y, false, this.minorTickThickness, this.minorTickLength, 0, 0, 0, 0xFF); + this.y.drawTicks(buffer, this.view, this.x, false, this.minorTickThickness, this.minorTickLength, 0, 0, 0, 0xFF); + this.x.drawTicks(buffer, this.view, this.y, true, this.majorTickThickness, this.majorTickLength, 0, 0, 0, 0xFF); + this.y.drawTicks(buffer, this.view, this.x, true, this.majorTickThickness, this.majorTickLength, 0, 0, 0, 0xFF); + } + + public static void drawData(GuiContext context, GraphView view, float[] xs, float[] ys, float d, int color) { + int r = Color.getRed(color); + int g = Color.getGreen(color); + int b = Color.getBlue(color); + int a = Color.getAlpha(color); + Platform.setupDrawColor(); + Platform.startDrawing(Platform.DrawMode.TRIANGLE_STRIP, Platform.VertexFormat.POS_COLOR, buffer -> { + for (int i = 0; i < xs.length - 1; i++) { + float x0 = view.transformXGraphToScreen(xs[i]); + float y0 = view.transformYGraphToScreen(ys[i]); + float x1 = view.transformXGraphToScreen(xs[i + 1]); + float y1 = view.transformYGraphToScreen(ys[i + 1]); + + float dx = x1 - x0; + float dy = y1 - y0; + float len = MathUtils.sqrt(dx * dx + dy * dy); + if (len == 0) continue; + dx /= len; + dy /= len; + + // perpendicular + float px = -dy; + float py = dx; + // thickness offset + float ox = px * (d * 0.5f); + float oy = py * (d * 0.5f); + + buffer.pos(x0 - ox, y0 - oy, 0).color(r, g, b, a).endVertex(); + buffer.pos(x0 + ox, y0 + oy, 0).color(r, g, b, a).endVertex(); + buffer.pos(x1 - ox, y1 - oy, 0).color(r, g, b, a).endVertex(); + buffer.pos(x1 + ox, y1 + oy, 0).color(r, g, b, a).endVertex(); + } + }); + } + + private boolean compute() { + if (!this.dirty) return false; + this.dirty = false; + this.x.compute(); + this.y.compute(); + return true; + } + + public GraphAxis getX() { + return x; + } + + public GraphAxis getY() { + return y; + } + + public GraphDrawable data(float[] x, float[] y) { + this.x.data = x; + this.y.data = y; + this.dirty |= this.x.autoLimits || this.y.autoLimits; + return this; + } + + public GraphDrawable xData(float[] x) { + return data(x, this.y.data); + } + + public GraphDrawable yData(float[] y) { + return data(this.x.data, y); + } + + public GraphDrawable autoXLim() { + this.x.autoLimits = true; + redraw(); + return this; + } + + public GraphDrawable autoYLim() { + this.y.autoLimits = true; + redraw(); + return this; + } + + public GraphDrawable xLim(float min, float max) { + this.x.min = min; + this.x.max = max; + this.x.autoLimits = false; + redraw(); + return this; + } + + public GraphDrawable yLim(float min, float max) { + this.y.min = min; + this.y.max = max; + this.y.autoLimits = false; + redraw(); + return this; + } + + public GraphDrawable majorTickStyle(float thickness, float length) { + this.majorTickThickness = thickness; + this.majorTickLength = length; + return this; + } + + public GraphDrawable minorTickStyle(float thickness, float length) { + this.minorTickThickness = thickness; + this.minorTickLength = length; + return this; + } + + public GraphDrawable xTickFinder(MajorTickFinder majorTickFinder, MinorTickFinder minorTickFinder) { + this.x.majorTickFinder = majorTickFinder; + this.x.minorTickFinder = minorTickFinder; + redraw(); + return this; + } + + public GraphDrawable yTickFinder(MajorTickFinder majorTickFinder, MinorTickFinder minorTickFinder) { + this.y.majorTickFinder = majorTickFinder; + this.y.minorTickFinder = minorTickFinder; + redraw(); + return this; + } + + public GraphDrawable xTickFinder(float majorMultiples, int minorTicksBetweenMajors) { + return xTickFinder(new AutoMajorTickFinder(majorMultiples), new AutoMinorTickFinder(minorTicksBetweenMajors)); + } + + public GraphDrawable yTickFinder(float majorMultiples, int minorTicksBetweenMajors) { + return yTickFinder(new AutoMajorTickFinder(majorMultiples), new AutoMinorTickFinder(minorTicksBetweenMajors)); + } + + public GraphDrawable backgroundColor(int color) { + if (color != 0 && Color.getAlpha(color) == 0) { + color = Color.withAlpha(color, 0xFF); + } + this.backgroundColor = color; + return this; + } + + public GraphDrawable lineThickness(float thickness) { + this.lineWidth = thickness; + return this; + } + + public GraphDrawable lineColor(int color) { + this.lineColor = color; + return this; + } + + public GraphDrawable majorGridStyle(float thickness, int color) { + this.gridLineWidth = thickness; + this.gridLineColor = color; + return this; + } + + public GraphDrawable minorGridStyle(float thickness, int color) { + this.minorGridLineWidth = thickness; + this.minorGridLineColor = color; + return this; + } +} diff --git a/src/main/java/com/cleanroommc/modularui/drawable/graph/GraphView.java b/src/main/java/com/cleanroommc/modularui/drawable/graph/GraphView.java new file mode 100644 index 000000000..f91c2752b --- /dev/null +++ b/src/main/java/com/cleanroommc/modularui/drawable/graph/GraphView.java @@ -0,0 +1,57 @@ +package com.cleanroommc.modularui.drawable.graph; + +import com.cleanroommc.modularui.utils.Interpolations; + +import org.jetbrains.annotations.ApiStatus; + +@ApiStatus.Experimental +public class GraphView { + + // screen rectangle + float sx0, sy0, sx1, sy1; + // graph rectangle + float gx0, gy0, gx1, gy1; + + boolean setScreen(float x0, float y0, float x1, float y1) { + if (x0 != this.sx0 || y0 != this.sy0 || x1 != this.sx1 || y1 != this.sy1) { + this.sx0 = x0; + this.sy0 = y0; + this.sx1 = x1; + this.sy1 = y1; + return true; + } + return false; + } + + void setGraph(float x0, float y0, float x1, float y1) { + this.gx0 = x0; + this.gy0 = y0; + this.gx1 = x1; + this.gy1 = y1; + } + + public float transformXGraphToScreen(float v) { + return transform(v, gx0, gx1, sx0, sx1); + } + + public float transformYGraphToScreen(float v) { + // gy0 and gy1 inverted on purpose + // screen y0 is top, graph y0 is bottom + return transform(v, gy1, gy0, sy0, sy1); + } + + public float transformXScreenToGraph(float v) { + return transform(v, sx0, sx1, gx0, gx1); + } + + public float transformYScreenToGraph(float v) { + // gy0 and gy1 inverted on purpose + // screen y0 is top, graph y0 is bottom + return transform(v, sy0, sy1, gy1, gy0); + } + + private float transform(float v, float fromMin, float fromMax, float toMin, float toMax) { + v = (v - fromMin) / (fromMax - fromMin); // reverse lerp + return Interpolations.lerp(toMin, toMax, v); + } +} diff --git a/src/main/java/com/cleanroommc/modularui/drawable/graph/MajorTickFinder.java b/src/main/java/com/cleanroommc/modularui/drawable/graph/MajorTickFinder.java new file mode 100644 index 000000000..741d18d97 --- /dev/null +++ b/src/main/java/com/cleanroommc/modularui/drawable/graph/MajorTickFinder.java @@ -0,0 +1,9 @@ +package com.cleanroommc.modularui.drawable.graph; + +import org.jetbrains.annotations.ApiStatus; + +@ApiStatus.Experimental +public interface MajorTickFinder { + + float[] find(float min, float max, float[] ticks); +} diff --git a/src/main/java/com/cleanroommc/modularui/drawable/graph/MinorTickFinder.java b/src/main/java/com/cleanroommc/modularui/drawable/graph/MinorTickFinder.java new file mode 100644 index 000000000..a892be4a4 --- /dev/null +++ b/src/main/java/com/cleanroommc/modularui/drawable/graph/MinorTickFinder.java @@ -0,0 +1,9 @@ +package com.cleanroommc.modularui.drawable.graph; + +import org.jetbrains.annotations.ApiStatus; + +@ApiStatus.Experimental +public interface MinorTickFinder { + + float[] find(float min, float max, float[] majorTicks, float[] ticks); +} diff --git a/src/main/java/com/cleanroommc/modularui/screen/ClientScreenHandler.java b/src/main/java/com/cleanroommc/modularui/screen/ClientScreenHandler.java index 0cc4e352f..96bdbf347 100644 --- a/src/main/java/com/cleanroommc/modularui/screen/ClientScreenHandler.java +++ b/src/main/java/com/cleanroommc/modularui/screen/ClientScreenHandler.java @@ -600,9 +600,9 @@ public static void drawDebugScreen(@Nullable ModularScreen muiScreen, @Nullable Area area = hovered.getArea(); IWidget parent = hovered.getParent(); - GuiDraw.drawBorder(0, 0, area.width, area.height, color, scale); + GuiDraw.drawBorderOutsideXYWH(0, 0, area.width, area.height, scale, color); if (hovered.hasParent()) { - GuiDraw.drawBorder(-area.rx, -area.ry, parent.getArea().width, parent.getArea().height, Color.withAlpha(color, 0.3f), scale); + GuiDraw.drawBorderOutsideXYWH(-area.rx, -area.ry, parent.getArea().width, parent.getArea().height, scale, Color.withAlpha(color, 0.3f)); } GlStateManager.popMatrix(); locatedHovered.unapplyMatrix(context); diff --git a/src/main/java/com/cleanroommc/modularui/test/TestGuis.java b/src/main/java/com/cleanroommc/modularui/test/TestGuis.java index 0b75f85a2..a79d6806e 100644 --- a/src/main/java/com/cleanroommc/modularui/test/TestGuis.java +++ b/src/main/java/com/cleanroommc/modularui/test/TestGuis.java @@ -14,6 +14,8 @@ import com.cleanroommc.modularui.drawable.GuiTextures; import com.cleanroommc.modularui.drawable.ItemDrawable; import com.cleanroommc.modularui.drawable.Rectangle; +import com.cleanroommc.modularui.drawable.UITexture; +import com.cleanroommc.modularui.drawable.graph.GraphDrawable; import com.cleanroommc.modularui.factory.ClientGUI; import com.cleanroommc.modularui.screen.CustomModularScreen; import com.cleanroommc.modularui.screen.ModularPanel; @@ -26,8 +28,10 @@ import com.cleanroommc.modularui.utils.Color; import com.cleanroommc.modularui.utils.GlStateManager; import com.cleanroommc.modularui.utils.ColorShade; +import com.cleanroommc.modularui.utils.FloatArrayMath; import com.cleanroommc.modularui.utils.Interpolation; import com.cleanroommc.modularui.utils.Interpolations; +import com.cleanroommc.modularui.utils.MathUtils; import com.cleanroommc.modularui.utils.Platform; import com.cleanroommc.modularui.utils.fakeworld.ArraySchema; import com.cleanroommc.modularui.utils.fakeworld.FakeEntity; @@ -59,6 +63,7 @@ import net.minecraft.item.ItemStack; import net.minecraft.util.EnumChatFormatting; +import com.google.common.base.CaseFormat; import org.apache.commons.lang3.tuple.Pair; import org.jetbrains.annotations.NotNull; @@ -73,6 +78,8 @@ public class TestGuis extends CustomModularScreen { + public static boolean withCode = false; + @Override public @NotNull ModularPanel buildUI(ModularGuiContext context) { // collect all test from all build methods in this class via reflection @@ -97,11 +104,21 @@ public class TestGuis extends CustomModularScreen { String name = m.getName(); if (name.startsWith("build")) name = name.substring(5); if (name.endsWith("UI")) name = name.substring(0, name.length() - 2); + String codeTextureName = CaseFormat.UPPER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE, name); name = name.replaceAll("([a-z])([A-Z])", "$1 $2"); return button(name) .onMousePressed(button -> { try { - ClientGUI.open(new ModularScreen((ModularPanel) m.invoke(null)).openParentOnClose(true)); + ModularPanel panel = (ModularPanel) m.invoke(null); + if (TestGuis.withCode) { + panel.child(UITexture.builder() + .location("gui/code/" + codeTextureName) + .build() + .asWidget() + .leftRel(1f) + .heightRel(1f)); + } + ClientGUI.open(new ModularScreen(panel).openParentOnClose(true)); } catch (IllegalAccessException | InvocationTargetException e) { ModularUI.LOGGER.throwing(e); } @@ -512,6 +529,20 @@ public static ModularPanel buildCollapseDisabledChildrenUI() { .hoverBackground(GuiTextures.MC_BUTTON_HOVERED)); } + public static @NotNull ModularPanel buildGraphUI() { + float[] x = FloatArrayMath.linspace(-5, 5, 100); + float[] y = FloatArrayMath.sin(x, null); + return new ModularPanel("graph") + .size(200, 150) + .padding(5) + .overlay(new GraphDrawable() + .yLim(-1.2f, 1.2f) + .autoXLim() + .xTickFinder(MathUtils.PI_HALF, 1) + .yTickFinder(0.2f, 1) + .data(x, y)); + } + private static class TestPanel extends ModularPanel { public TestPanel(String name) { diff --git a/src/main/java/com/cleanroommc/modularui/utils/FloatArrayMath.java b/src/main/java/com/cleanroommc/modularui/utils/FloatArrayMath.java new file mode 100644 index 000000000..a89237a91 --- /dev/null +++ b/src/main/java/com/cleanroommc/modularui/utils/FloatArrayMath.java @@ -0,0 +1,294 @@ +package com.cleanroommc.modularui.utils; + +import org.jetbrains.annotations.Nullable; + +import java.util.Arrays; + +/** + * A helper class providing math operations on 1D float arrays similar to numpy. + */ +public class FloatArrayMath { + + public static final float[] EMPTY = new float[0]; + + /** + * Creates an array of length n filled with zeros. + * + * @param n length + * @return zero filled array + */ + public static float[] zeros(int n) { + return new float[n]; + } + + /** + * Creates an array of length n filled with f. + * + * @param n length + * @param f fill value + * @return f filled array + */ + public static float[] full(int n, float f) { + float[] arr = new float[n]; + Arrays.fill(arr, f); + return arr; + } + + + /** + * Creates an array of length n filled with ones. + * + * @param n length + * @return one filled array + */ + public static float[] ones(int n) { + return full(n, 1); + } + + public static float[] copyInto(float[] src, float @Nullable [] res) { + if (src == res) return res; + if (res == null) res = new float[src.length]; + int n = Math.min(src.length, res.length); + System.arraycopy(src, 0, res, 0, n); + return res; + } + + public static float[] subArray(float[] src, int start, int length) { + float[] res = new float[length]; + System.arraycopy(src, start, res, 0, length); + return res; + } + + public static float[] linspace(float start, float stop) { + return linspace(start, stop, 50, true); + } + + public static float[] linspace(float start, float stop, boolean includeEndpoint) { + return linspace(start, stop, 50, includeEndpoint); + } + + public static float[] linspace(float start, float stop, int n) { + return linspace(start, stop, n, true); + } + + /** + * Creates an evenly spaced array over a specified interval. + * + * @param start start of interval + * @param stop stop of interval + * @param n sample size = array length + * @param includeEndpoint true if stop should be included at the end of the array + * @return evenly spaced array over interval + */ + public static float[] linspace(float start, float stop, int n, boolean includeEndpoint) { + float[] arr = new float[n]; + float step = (stop - start) / (includeEndpoint ? n + 1 : n); + int s = n; + if (includeEndpoint) { + arr[n - 1] = stop; + s--; + } + for (int i = 0; i < s; i++) { + arr[i] = start + step * i; + } + return arr; + } + + public static float[] arange(float stop, float step) { + return arange(0, stop, step); + } + + public static float[] arange(float start, float stop, float step) { + float[] arr = new float[(int) Math.ceil((stop - start) / step)]; + for (int i = 0; i < arr.length; i++) { + arr[i] = start + step * i; + } + return arr; + } + + /** + * Returns the index of the largest value in the array. If array is empty, -1 is returned. + * + * @param arr array + * @return index of largest value + */ + public static int argMax(float[] arr) { + if (arr.length == 0) return -1; + if (arr.length == 1) return 0; + if (arr.length == 2) return arr[0] >= arr[1] ? 0 : 1; + int index = 0; + for (int i = 1; i < arr.length; i++) { + if (arr[i] > arr[index]) index = i; + } + return index; + } + + /** + * Returns the index of the smallest value in the array. If array is empty, -1 is returned. + * + * @param arr array + * @return index of smallest value + */ + public static int argMin(float[] arr) { + if (arr.length == 0) return -1; + if (arr.length == 1) return 0; + if (arr.length == 2) return arr[0] <= arr[1] ? 0 : 1; + int index = 0; + for (int i = 1; i < arr.length; i++) { + if (arr[i] < arr[index]) index = i; + } + return index; + } + + /** + * Returns the largest value in the array. If array is empty, 0 is returned. + * + * @param arr array + * @return largest value + */ + public static float max(float[] arr) { + int i = argMax(arr); + return i < 0 ? 0 : arr[i]; + } + + /** + * Returns the smallest value in the array. If array is empty, 0 is returned. + * + * @param arr array + * @return smallest value + */ + public static float min(float[] arr) { + int i = argMin(arr); + return i < 0 ? 0 : arr[i]; + } + + /** + * Adds an operand to every element of the source array. + * + * @param src source array + * @param op operand + * @param res result array. If this is null a new array is created. This can be the same as src. + * @return result array + */ + public static float[] plus(float[] src, float op, float @Nullable [] res) { + if (res == null) res = new float[src.length]; + int n = Math.min(src.length, res.length); + for (int i = 0; i < n; i++) res[i] = src[i] + op; + return res; + } + + /** + * Adds each element of the operand to the element at the corresponding index of the source array. + * + * @param src source array + * @param op operands + * @param res result array. If this is null a new array is created. This can be the same as src. + * @return result array + */ + public static float[] plus(float[] src, float[] op, float @Nullable [] res) { + if (src.length != op.length) throw new IllegalArgumentException("Can't add arrays of different size."); + if (res == null) res = new float[src.length]; + int n = Math.min(src.length, res.length); + for (int i = 0; i < n; i++) res[i] = src[i] + op[i]; + return res; + } + + /** + * Multiplies an operand with every element of the source array. + * + * @param src source array + * @param op operand + * @param res result array. If this is null a new array is created. This can be the same as src. + * @return result array + */ + public static float[] mult(float[] src, float op, float @Nullable [] res) { + if (res == null) res = new float[src.length]; + int n = Math.min(src.length, res.length); + for (int i = 0; i < n; i++) res[i] = src[i] * op; + return res; + } + + public static float[] div(float[] src, float op, float @Nullable [] res) { + return mult(src, 1 / op, res); + } + + public static float[] diff(float[] src) { + if (src.length < 2) return EMPTY; + if (src.length == 2) return new float[]{src[1] - src[0]}; + float[] res = new float[src.length - 1]; + for (int i = 0; i < res.length; i++) { + res[i] = src[i + 1] - src[i]; + } + return res; + } + + public static float[] applyEach(float[] src, UnaryFloatOperator op, float @Nullable [] res) { + if (res == null) res = new float[src.length]; + int n = Math.min(src.length, res.length); + for (int i = 0; i < n; i++) res[i] = op.apply(src[i]); + return res; + } + + public static float[] applyEach(float[] src, float[] operands, BinaryFloatOperator op, float @Nullable [] res) { + if (src.length != operands.length) throw new IllegalArgumentException("Can't apply operator to operands of different size."); + if (res == null) res = new float[src.length]; + int n = Math.min(src.length, res.length); + for (int i = 0; i < n; i++) res[i] = op.apply(src[i], operands[i]); + return res; + } + + public static float[] applyEach(float[] src, float[] operands1, float[] operands2, TernaryFloatOperator op, float @Nullable [] res) { + if (src.length != operands1.length || src.length != operands2.length) { + throw new IllegalArgumentException("Can't apply operator to operands of different size."); + } + if (res == null) res = new float[src.length]; + int n = Math.min(src.length, res.length); + for (int i = 0; i < n; i++) res[i] = op.apply(src[i], operands1[i], operands2[i]); + return res; + } + + public static float[] abs(float[] src, float @Nullable [] res) { + return applyEach(src, Math::abs, res); + } + + public static float[] sin(float[] src, float @Nullable [] res) { + return applyEach(src, MathUtils::sin, res); + } + + public static float[] cos(float[] src, float @Nullable [] res) { + return applyEach(src, MathUtils::cos, res); + } + + public static float[] clamp(float[] src, float min, float max, float @Nullable [] res) { + return applyEach(src, v -> MathUtils.clamp(v, min, max), res); + } + + public static float[] polynomial(float[] src, float[] coeff, float @Nullable [] res) { + if (coeff.length == 0) return copyInto(src, res); + if (coeff.length == 1) return plus(src, coeff[0], res); + return applyEach(src, x -> { + float y = 0; + y += coeff[0]; + if (coeff.length == 2) return y + x * coeff[1]; + for (int i = 1; i < coeff.length; i++) { + y += (float) (Math.pow(x, i) * coeff[i]); + } + return y; + }, res); + } + + public interface UnaryFloatOperator { + + float apply(float v); + } + + public interface BinaryFloatOperator { + + float apply(float v, float op); + } + + public interface TernaryFloatOperator { + + float apply(float v, float op1, float op2); + } +} From 7eb933db7ca6b29049ccaf9eee9d3382fe6ad4ae Mon Sep 17 00:00:00 2001 From: brachy84 Date: Mon, 8 Dec 2025 21:53:00 +0100 Subject: [PATCH 19/50] ItemDisplayWidget implements RecipeViewerIngredientProvider (cherry picked from commit 1a3f4d474d1c2a8e305103bde4118c5154cc5b53) --- .../cleanroommc/modularui/widgets/ItemDisplayWidget.java | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/cleanroommc/modularui/widgets/ItemDisplayWidget.java b/src/main/java/com/cleanroommc/modularui/widgets/ItemDisplayWidget.java index 053bf9b13..3f63382c2 100644 --- a/src/main/java/com/cleanroommc/modularui/widgets/ItemDisplayWidget.java +++ b/src/main/java/com/cleanroommc/modularui/widgets/ItemDisplayWidget.java @@ -4,6 +4,7 @@ import com.cleanroommc.modularui.api.value.ISyncOrValue; import com.cleanroommc.modularui.api.value.IValue; import com.cleanroommc.modularui.drawable.GuiDraw; +import com.cleanroommc.modularui.integration.recipeviewer.RecipeViewerIngredientProvider; import com.cleanroommc.modularui.screen.viewport.ModularGuiContext; import com.cleanroommc.modularui.theme.WidgetThemeEntry; import com.cleanroommc.modularui.utils.Alignment; @@ -15,13 +16,14 @@ import net.minecraft.item.ItemStack; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; /** * An item slot which only purpose is to display an item stack. * The displayed item stack can be supplied directly, by an {@link ObjectValue} dynamically or by a {@link GenericSyncValue} synced. * Players can not interact with this widget in any form. */ -public class ItemDisplayWidget extends Widget { +public class ItemDisplayWidget extends Widget implements RecipeViewerIngredientProvider { private IValue value; private boolean displayAmount = false; @@ -79,4 +81,9 @@ public ItemDisplayWidget displayAmount(boolean displayAmount) { this.displayAmount = displayAmount; return this; } + + @Override + public @Nullable Object getIngredient() { + return value.getValue(); + } } From 3b054f636a85a60f9b774dfe0c2e40e4d63a8bfb Mon Sep 17 00:00:00 2001 From: Zorbatron <46525467+Zorbatron@users.noreply.github.com> Date: Mon, 8 Dec 2025 17:16:56 -0500 Subject: [PATCH 20/50] Add overload of callSyncedAction with no packet arg for actions that don't need any data (#183) (cherry picked from commit 5d6a380d98d41f245145ef2b33c33adc9f9383dc) --- .../cleanroommc/modularui/value/sync/PanelSyncManager.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/java/com/cleanroommc/modularui/value/sync/PanelSyncManager.java b/src/main/java/com/cleanroommc/modularui/value/sync/PanelSyncManager.java index 039fdcccc..4dc7f9402 100644 --- a/src/main/java/com/cleanroommc/modularui/value/sync/PanelSyncManager.java +++ b/src/main/java/com/cleanroommc/modularui/value/sync/PanelSyncManager.java @@ -338,6 +338,10 @@ public void callSyncedAction(String mapKey, Consumer packetBuilder callSyncedAction(mapKey, packet); } + public void callSyncedAction(String mapKey) { + callSyncedAction(mapKey, new PacketBuffer(Unpooled.buffer(0))); + } + @Override public T getOrCreateSyncHandler(String name, int id, Class clazz, Supplier supplier) { SyncHandler syncHandler = findSyncHandlerNullable(name, id); From 987ef920374901ba62ed5798e29918411f361d87 Mon Sep 17 00:00:00 2001 From: brachy84 Date: Tue, 9 Dec 2025 14:54:29 +0100 Subject: [PATCH 21/50] improve graph geometry, cache graph vertices, support multiple plots per graph (cherry picked from commit df4387a8622b979e77cb7bfe592c6c0416f12104) --- .../drawable/graph/AutoMajorTickFinder.java | 38 +++- .../modularui/drawable/graph/GraphAxis.java | 61 ++++-- .../drawable/graph/GraphDrawable.java | 194 ++++++++--------- .../modularui/drawable/graph/GraphView.java | 47 ++++- .../modularui/drawable/graph/Plot.java | 197 ++++++++++++++++++ .../cleanroommc/modularui/test/TestGuis.java | 5 +- 6 files changed, 419 insertions(+), 123 deletions(-) create mode 100644 src/main/java/com/cleanroommc/modularui/drawable/graph/Plot.java diff --git a/src/main/java/com/cleanroommc/modularui/drawable/graph/AutoMajorTickFinder.java b/src/main/java/com/cleanroommc/modularui/drawable/graph/AutoMajorTickFinder.java index 0e5fb6361..9105a1be7 100644 --- a/src/main/java/com/cleanroommc/modularui/drawable/graph/AutoMajorTickFinder.java +++ b/src/main/java/com/cleanroommc/modularui/drawable/graph/AutoMajorTickFinder.java @@ -5,26 +5,56 @@ @ApiStatus.Experimental public class AutoMajorTickFinder implements MajorTickFinder { - private float multiple; + private final boolean autoAdjust; + private float multiple = 10; + + public AutoMajorTickFinder(boolean autoAdjust) { + this.autoAdjust = autoAdjust; + } public AutoMajorTickFinder(float multiple) { + this.autoAdjust = false; this.multiple = multiple; } @Override public float[] find(float min, float max, float[] ticks) { - int s = (int) Math.ceil((max - min) / multiple) + 1; + int s = (int) Math.ceil((max - min) / multiple) + 2; if (s > ticks.length) ticks = new float[s]; float next = (float) (Math.floor(min / multiple) * multiple); for (int i = 0; i < s; i++) { + ticks[i] = next; if (next > max) { - s--; + s = i + 1; break; } - ticks[i] = next; next += multiple; } if (ticks.length > s) ticks[s] = Float.NaN; return ticks; } + + void calculateAutoTickMultiple(float min, float max) { + float step = (max - min) / 5; + if (step < 1) { + int significantPlaces = (int) Math.abs(Math.log10(step)) + 2; + float ten = (float) Math.pow(10, significantPlaces); + step = (int) (step * ten + 0.2f) / ten; + } else if (step == 1) { + step = 0.2f; + } else { + int significantPlaces = (int) Math.log10(step) - 1; + float ten = (float) Math.pow(10, significantPlaces); + step = (int) (step / ten + 0.2f) * ten; + } + setMultiple(step); + } + + public boolean isAutoAdjust() { + return autoAdjust; + } + + public void setMultiple(float multiple) { + this.multiple = multiple; + } } diff --git a/src/main/java/com/cleanroommc/modularui/drawable/graph/GraphAxis.java b/src/main/java/com/cleanroommc/modularui/drawable/graph/GraphAxis.java index 9da775a1c..67c5b2cfa 100644 --- a/src/main/java/com/cleanroommc/modularui/drawable/graph/GraphAxis.java +++ b/src/main/java/com/cleanroommc/modularui/drawable/graph/GraphAxis.java @@ -11,6 +11,7 @@ import org.jetbrains.annotations.ApiStatus; import java.text.DecimalFormat; +import java.util.List; @ApiStatus.Experimental public class GraphAxis { @@ -27,21 +28,43 @@ public class GraphAxis { public float[] minorTicks = new float[16]; public TextRenderer.Line[] tickLabels = new TextRenderer.Line[8]; private float maxLabelWidth = 0; - public MajorTickFinder majorTickFinder = new AutoMajorTickFinder(10); - public MinorTickFinder minorTickFinder = new AutoMinorTickFinder(1); + public MajorTickFinder majorTickFinder = new AutoMajorTickFinder(true); + public MinorTickFinder minorTickFinder = new AutoMinorTickFinder(2); public String label; public float min, max; public boolean autoLimits = true; - public float[] data; public GraphAxis(GuiAxis axis) { this.axis = axis; } - void compute() { + void compute(List plots) { if (this.autoLimits) { - this.min = FloatArrayMath.min(this.data); - this.max = FloatArrayMath.max(this.data); + if (plots.isEmpty()) { + this.min = 0; + this.max = 0; + } else if (plots.size() == 1) { + this.min = FloatArrayMath.min(plots.get(0).getData(this.axis)); + this.max = FloatArrayMath.max(plots.get(0).getData(this.axis)); + } else { + float min = Float.MAX_VALUE, max = Float.MIN_VALUE; + for (Plot plot : plots) { + float m = FloatArrayMath.min(plot.getData(this.axis)); + if (m < min) min = m; + m = FloatArrayMath.max(plot.getData(this.axis)); + if (m > max) max = m; + } + this.min = min; + this.max = max; + } + if (this.axis.isVertical()) { + float padding = (this.max - this.min) * 0.05f; + this.max += padding; + this.min -= padding; + } + } + if (this.majorTickFinder instanceof AutoMajorTickFinder tickFinder && tickFinder.isAutoAdjust()) { + tickFinder.calculateAutoTickMultiple(this.min, this.max); } this.majorTicks = this.majorTickFinder.find(this.min, this.max, this.majorTicks); this.minorTicks = this.minorTickFinder.find(this.min, this.max, this.majorTicks, this.minorTicks); @@ -52,7 +75,7 @@ void compute() { textRenderer.setScale(TICK_LABEL_SCALE); this.maxLabelWidth = 0; float maxDiff = FloatArrayMath.max(FloatArrayMath.diff(this.majorTicks)); - int significantPlaces = (int) Math.abs(Math.log10(maxDiff)) + 1; + int significantPlaces = (int) Math.abs(Math.log10(maxDiff)) + 2; DecimalFormat format = new DecimalFormat(); format.setMaximumFractionDigits(significantPlaces); for (int i = 0; i < this.tickLabels.length; i++) { @@ -87,12 +110,12 @@ void drawGridLines(BufferBuilder buffer, GraphView view, GraphAxis other, boolea float[] pos = major ? this.majorTicks : this.minorTicks; float dHalf = d / 2; if (axis.isHorizontal()) { - float otherMin = view.transformYGraphToScreen(other.max); - float otherMax = view.transformYGraphToScreen(other.min); + float otherMin = view.g2sY(other.max); + float otherMax = view.g2sY(other.min); drawLinesOnHorizontal(buffer, view, pos, dHalf, otherMin, otherMax, r, g, b, a); } else { - float otherMin = view.transformXGraphToScreen(other.min); - float otherMax = view.transformXGraphToScreen(other.max); + float otherMin = view.g2sX(other.min); + float otherMax = view.g2sX(other.max); drawLinesOnVertical(buffer, view, pos, dHalf, otherMin, otherMax, r, g, b, a); } } @@ -101,10 +124,10 @@ void drawTicks(BufferBuilder buffer, GraphView view, GraphAxis other, boolean ma float[] pos = major ? this.majorTicks : this.minorTicks; float dHalf = thickness / 2; if (axis.isHorizontal()) { - float otherMin = view.transformYGraphToScreen(other.min); + float otherMin = view.g2sY(other.min); drawLinesOnHorizontal(buffer, view, pos, dHalf, otherMin - length, otherMin, r, g, b, a); } else { - float otherMin = view.transformXGraphToScreen(other.min); + float otherMin = view.g2sX(other.min); drawLinesOnVertical(buffer, view, pos, dHalf, otherMin, otherMin + length, r, g, b, a); } } @@ -115,7 +138,7 @@ private void drawLinesOnHorizontal(BufferBuilder buffer, GraphView view, float[] if (Float.isNaN(p)) break; if (p < min || p > max) continue; - p = view.transformXGraphToScreen(p); + p = view.g2sX(p); float x0 = p - dHalf; float x1 = p + dHalf; @@ -129,7 +152,7 @@ private void drawLinesOnVertical(BufferBuilder buffer, GraphView view, float[] p if (Float.isNaN(p)) break; if (p < min || p > max) continue; - p = view.transformYGraphToScreen(p); + p = view.g2sY(p); float y0 = p - dHalf; float y1 = p + dHalf; @@ -142,23 +165,23 @@ void drawLabels(GraphView view, GraphAxis other) { if (axis.isHorizontal()) { textRenderer.setScale(TICK_LABEL_SCALE); textRenderer.setAlignment(Alignment.TopCenter, 100); - float y = view.transformYGraphToScreen(other.min) + TICK_LABEL_OFFSET; + float y = view.g2sY(other.min) + TICK_LABEL_OFFSET; for (int i = 0; i < this.majorTicks.length; i++) { float pos = this.majorTicks[i]; if (Float.isNaN(pos)) break; if (pos < min || pos > max) continue; - textRenderer.setPos((int) (view.transformXGraphToScreen(pos) - 50), (int) y); + textRenderer.setPos((int) (view.g2sX(pos) - 50), (int) y); textRenderer.draw(this.tickLabels[i].getText()); } } else { textRenderer.setScale(TICK_LABEL_SCALE); textRenderer.setAlignment(Alignment.CenterRight, this.maxLabelWidth, 20); - float x = view.transformXGraphToScreen(other.min) - TICK_LABEL_OFFSET - this.maxLabelWidth; + float x = view.g2sX(other.min) - TICK_LABEL_OFFSET - this.maxLabelWidth; for (int i = 0; i < this.majorTicks.length; i++) { float pos = this.majorTicks[i]; if (Float.isNaN(pos)) break; if (pos < min || pos > max) continue; - textRenderer.setPos((int) x, (int) (view.transformYGraphToScreen(pos) - 10)); + textRenderer.setPos((int) x, (int) (view.g2sY(pos) - 10)); textRenderer.draw(this.tickLabels[i].getText()); } } diff --git a/src/main/java/com/cleanroommc/modularui/drawable/graph/GraphDrawable.java b/src/main/java/com/cleanroommc/modularui/drawable/graph/GraphDrawable.java index 9966fad81..bf0676bb7 100644 --- a/src/main/java/com/cleanroommc/modularui/drawable/graph/GraphDrawable.java +++ b/src/main/java/com/cleanroommc/modularui/drawable/graph/GraphDrawable.java @@ -6,13 +6,13 @@ import com.cleanroommc.modularui.screen.viewport.GuiContext; import com.cleanroommc.modularui.theme.WidgetTheme; import com.cleanroommc.modularui.utils.Color; -import com.cleanroommc.modularui.utils.MathUtils; import com.cleanroommc.modularui.utils.Platform; -import net.minecraft.client.renderer.BufferBuilder; - import org.jetbrains.annotations.ApiStatus; +import java.util.ArrayList; +import java.util.List; + @ApiStatus.Experimental public class GraphDrawable implements IDrawable { @@ -20,24 +20,21 @@ public class GraphDrawable implements IDrawable { //private IDrawable background; private int backgroundColor = Color.WHITE.main; - private float lineWidth = 2; - private int lineColor = Color.BLUE_ACCENT.main; - private boolean borderTop = true, borderLeft = true, borderBottom = false, borderRight = false; - + private float majorTickThickness = 1f, majorTickLength = 3f, minorTickThickness = 0.5f, minorTickLength = 1.5f; private float gridLineWidth = 0.5f; private int gridLineColor = Color.withAlpha(Color.BLACK.main, 0.4f); - private float minorGridLineWidth = 0.25f; + private float minorGridLineWidth = 0f; private int minorGridLineColor = Color.withAlpha(Color.BLACK.main, 0.15f); private final GraphAxis x = new GraphAxis(GuiAxis.X), y = new GraphAxis(GuiAxis.Y); - - private float majorTickThickness = 0.5f, majorTickLength = 1f, minorTickThickness = 0.25f, minorTickLength = 0.5f; + private final List plots = new ArrayList<>(); private boolean dirty = true; public void redraw() { this.dirty = true; + for (Plot plot : this.plots) plot.redraw(); } @Override @@ -46,87 +43,58 @@ public void draw(GuiContext context, int x, int y, int width, int height, Widget this.x.applyPadding(this.view); this.y.applyPadding(this.view); this.view.setGraph(this.x.min, this.y.min, this.x.max, this.y.max); + this.view.postResize(); } - if (this.backgroundColor != 0) { GuiDraw.drawRect(this.view.sx0, this.view.sy0, this.view.sx1 - this.view.sx0, this.view.sy1 - this.view.sy0, this.backgroundColor); } - Platform.setupDrawColor(); - Platform.startDrawing(Platform.DrawMode.QUADS, Platform.VertexFormat.POS_COLOR, buffer -> { - drawGrid(context, buffer); - }); - drawData(context, this.view, this.x.data, this.y.data, this.lineWidth, this.lineColor); - + drawGrid(context); + for (Plot plot : this.plots) { + plot.draw(this.view); + } + drawTicks(context); GuiDraw.drawBorderOutsideLTRB(this.view.sx0, this.view.sy0, this.view.sx1, this.view.sy1, 0.5f, Color.BLACK.main); this.x.drawLabels(this.view, this.y); this.y.drawLabels(this.view, this.x); } - public void drawGrid(GuiContext context, BufferBuilder buffer) { - if (this.minorGridLineWidth > 0) { - int r = Color.getRed(this.minorGridLineColor); - int g = Color.getGreen(this.minorGridLineColor); - int b = Color.getBlue(this.minorGridLineColor); - int a = Color.getAlpha(this.minorGridLineColor); - this.x.drawGridLines(buffer, this.view, this.y, false, this.minorGridLineWidth, r, g, b, a); - this.y.drawGridLines(buffer, this.view, this.x, false, this.minorGridLineWidth, r, g, b, a); - } - if (this.gridLineWidth > 0) { - int r = Color.getRed(this.gridLineColor); - int g = Color.getGreen(this.gridLineColor); - int b = Color.getBlue(this.gridLineColor); - int a = Color.getAlpha(this.gridLineColor); - this.x.drawGridLines(buffer, this.view, this.y, true, this.gridLineWidth, r, g, b, a); - this.y.drawGridLines(buffer, this.view, this.x, true, this.gridLineWidth, r, g, b, a); - } - - this.x.drawTicks(buffer, this.view, this.y, false, this.minorTickThickness, this.minorTickLength, 0, 0, 0, 0xFF); - this.y.drawTicks(buffer, this.view, this.x, false, this.minorTickThickness, this.minorTickLength, 0, 0, 0, 0xFF); - this.x.drawTicks(buffer, this.view, this.y, true, this.majorTickThickness, this.majorTickLength, 0, 0, 0, 0xFF); - this.y.drawTicks(buffer, this.view, this.x, true, this.majorTickThickness, this.majorTickLength, 0, 0, 0, 0xFF); + public void drawGrid(GuiContext context) { + if (this.gridLineWidth < 0 && this.minorGridLineWidth < 0) return; + Platform.startDrawing(Platform.DrawMode.QUADS, Platform.VertexFormat.POS_COLOR, buffer -> { + if (this.minorGridLineWidth > 0) { + int r = Color.getRed(this.minorGridLineColor); + int g = Color.getGreen(this.minorGridLineColor); + int b = Color.getBlue(this.minorGridLineColor); + int a = Color.getAlpha(this.minorGridLineColor); + this.x.drawGridLines(buffer, this.view, this.y, false, this.minorGridLineWidth, r, g, b, a); + this.y.drawGridLines(buffer, this.view, this.x, false, this.minorGridLineWidth, r, g, b, a); + } + if (this.gridLineWidth > 0) { + int r = Color.getRed(this.gridLineColor); + int g = Color.getGreen(this.gridLineColor); + int b = Color.getBlue(this.gridLineColor); + int a = Color.getAlpha(this.gridLineColor); + this.x.drawGridLines(buffer, this.view, this.y, true, this.gridLineWidth, r, g, b, a); + this.y.drawGridLines(buffer, this.view, this.x, true, this.gridLineWidth, r, g, b, a); + } + }); } - public static void drawData(GuiContext context, GraphView view, float[] xs, float[] ys, float d, int color) { - int r = Color.getRed(color); - int g = Color.getGreen(color); - int b = Color.getBlue(color); - int a = Color.getAlpha(color); - Platform.setupDrawColor(); - Platform.startDrawing(Platform.DrawMode.TRIANGLE_STRIP, Platform.VertexFormat.POS_COLOR, buffer -> { - for (int i = 0; i < xs.length - 1; i++) { - float x0 = view.transformXGraphToScreen(xs[i]); - float y0 = view.transformYGraphToScreen(ys[i]); - float x1 = view.transformXGraphToScreen(xs[i + 1]); - float y1 = view.transformYGraphToScreen(ys[i + 1]); - - float dx = x1 - x0; - float dy = y1 - y0; - float len = MathUtils.sqrt(dx * dx + dy * dy); - if (len == 0) continue; - dx /= len; - dy /= len; - - // perpendicular - float px = -dy; - float py = dx; - // thickness offset - float ox = px * (d * 0.5f); - float oy = py * (d * 0.5f); - - buffer.pos(x0 - ox, y0 - oy, 0).color(r, g, b, a).endVertex(); - buffer.pos(x0 + ox, y0 + oy, 0).color(r, g, b, a).endVertex(); - buffer.pos(x1 - ox, y1 - oy, 0).color(r, g, b, a).endVertex(); - buffer.pos(x1 + ox, y1 + oy, 0).color(r, g, b, a).endVertex(); - } + public void drawTicks(GuiContext context) { + Platform.startDrawing(Platform.DrawMode.QUADS, Platform.VertexFormat.POS_COLOR, buffer -> { + this.x.drawTicks(buffer, this.view, this.y, false, this.minorTickThickness, this.minorTickLength, 0, 0, 0, 0xFF); + this.y.drawTicks(buffer, this.view, this.x, false, this.minorTickThickness, this.minorTickLength, 0, 0, 0, 0xFF); + this.x.drawTicks(buffer, this.view, this.y, true, this.majorTickThickness, this.majorTickLength, 0, 0, 0, 0xFF); + this.y.drawTicks(buffer, this.view, this.x, true, this.majorTickThickness, this.majorTickLength, 0, 0, 0, 0xFF); }); } private boolean compute() { if (!this.dirty) return false; this.dirty = false; - this.x.compute(); - this.y.compute(); + this.x.compute(this.plots); + this.y.compute(this.plots); return true; } @@ -138,21 +106,6 @@ public GraphAxis getY() { return y; } - public GraphDrawable data(float[] x, float[] y) { - this.x.data = x; - this.y.data = y; - this.dirty |= this.x.autoLimits || this.y.autoLimits; - return this; - } - - public GraphDrawable xData(float[] x) { - return data(x, this.y.data); - } - - public GraphDrawable yData(float[] y) { - return data(this.x.data, y); - } - public GraphDrawable autoXLim() { this.x.autoLimits = true; redraw(); @@ -223,16 +176,35 @@ public GraphDrawable backgroundColor(int color) { return this; } - public GraphDrawable lineThickness(float thickness) { - this.lineWidth = thickness; - return this; + public GraphDrawable plot(float[] x, float[] y) { + return plot(new Plot().data(x, y)); + } + + public GraphDrawable plot(float[] x, float[] y, int color) { + return plot(new Plot() + .data(x, y) + .color(color)); + } + + public GraphDrawable plot(float[] x, float[] y, float thickness) { + return plot(new Plot() + .data(x, y) + .thickness(thickness)); + } + + public GraphDrawable plot(float[] x, float[] y, float thickness, int color) { + return plot(new Plot() + .data(x, y) + .thickness(thickness) + .color(color)); } - public GraphDrawable lineColor(int color) { - this.lineColor = color; + public GraphDrawable plot(Plot plot) { + this.plots.add(plot); return this; } + public GraphDrawable majorGridStyle(float thickness, int color) { this.gridLineWidth = thickness; this.gridLineColor = color; @@ -244,4 +216,40 @@ public GraphDrawable minorGridStyle(float thickness, int color) { this.minorGridLineColor = color; return this; } + + public GraphDrawable disableMajorGrid() { + return majorGridLineThickness(0); + } + + public GraphDrawable disableMinorGrid() { + return minorGridLineThickness(0); + } + + public GraphDrawable enableMajorGrid() { + return majorGridLineThickness(0.5f); + } + + public GraphDrawable enableMinorGrid() { + return majorGridLineThickness(0.25f); + } + + public GraphDrawable majorGridLineThickness(float thickness) { + this.gridLineWidth = thickness; + return this; + } + + public GraphDrawable minorGridLineThickness(float thickness) { + this.minorGridLineWidth = thickness; + return this; + } + + public GraphDrawable majorGridLineColor(int color) { + this.gridLineColor = color; + return this; + } + + public GraphDrawable minorGridLineColor(int color) { + this.minorGridLineColor = color; + return this; + } } diff --git a/src/main/java/com/cleanroommc/modularui/drawable/graph/GraphView.java b/src/main/java/com/cleanroommc/modularui/drawable/graph/GraphView.java index f91c2752b..ca06f17e3 100644 --- a/src/main/java/com/cleanroommc/modularui/drawable/graph/GraphView.java +++ b/src/main/java/com/cleanroommc/modularui/drawable/graph/GraphView.java @@ -12,6 +12,23 @@ public class GraphView { // graph rectangle float gx0, gy0, gx1, gy1; + float zeroX, zeroY; + + void postResize() { + float w = sx1 - sx0, h = sy1 - sy0; + if (w > h) { + float d = w - h; + sx0 += d / 2; + sx1 -= d / 2; + } else if (h > w) { + float d = h - w; + sy0 += d / 2; + sy1 -= d / 2; + } + this.zeroX = g2sX(0); + this.zeroY = g2sY(0); + } + boolean setScreen(float x0, float y0, float x1, float y1) { if (x0 != this.sx0 || y0 != this.sy0 || x1 != this.sx1 || y1 != this.sy1) { this.sx0 = x0; @@ -28,23 +45,33 @@ void setGraph(float x0, float y0, float x1, float y1) { this.gy0 = y0; this.gx1 = x1; this.gy1 = y1; + this.zeroX = g2sX(0); + this.zeroY = g2sY(0); } - public float transformXGraphToScreen(float v) { + public float g2sX(float v) { return transform(v, gx0, gx1, sx0, sx1); } - public float transformYGraphToScreen(float v) { + public float g2sY(float v) { // gy0 and gy1 inverted on purpose // screen y0 is top, graph y0 is bottom return transform(v, gy1, gy0, sy0, sy1); } - public float transformXScreenToGraph(float v) { + public float g2sScaleX() { + return scale(gx0, gx1, sx0, sx1); + } + + public float g2sScaleY() { + return scale(gy1, gy0, sy0, sy1); + } + + public float s2gX(float v) { return transform(v, sx0, sx1, gx0, gx1); } - public float transformYScreenToGraph(float v) { + public float s2gY(float v) { // gy0 and gy1 inverted on purpose // screen y0 is top, graph y0 is bottom return transform(v, sy0, sy1, gy1, gy0); @@ -54,4 +81,16 @@ private float transform(float v, float fromMin, float fromMax, float toMin, floa v = (v - fromMin) / (fromMax - fromMin); // reverse lerp return Interpolations.lerp(toMin, toMax, v); } + + private float scale(float fromMin, float fromMax, float toMin, float toMax) { + return (toMax - toMin) / (fromMax - fromMin); + } + + public float getZeroX() { + return zeroX; + } + + public float getZeroY() { + return zeroY; + } } diff --git a/src/main/java/com/cleanroommc/modularui/drawable/graph/Plot.java b/src/main/java/com/cleanroommc/modularui/drawable/graph/Plot.java new file mode 100644 index 000000000..348743a00 --- /dev/null +++ b/src/main/java/com/cleanroommc/modularui/drawable/graph/Plot.java @@ -0,0 +1,197 @@ +package com.cleanroommc.modularui.drawable.graph; + +import com.cleanroommc.modularui.api.GuiAxis; +import com.cleanroommc.modularui.drawable.GuiDraw; +import com.cleanroommc.modularui.utils.Color; +import com.cleanroommc.modularui.utils.FloatArrayMath; +import com.cleanroommc.modularui.utils.Interpolations; +import com.cleanroommc.modularui.utils.MathUtils; +import com.cleanroommc.modularui.utils.Platform; + +public class Plot { + + float[] xs = FloatArrayMath.EMPTY; + float[] ys = FloatArrayMath.EMPTY; + float thickness = 1f; + int color = Color.BLUE_ACCENT.main; + + private float[] vertexBuffer; + private boolean dirty = true; + + public void redraw() { + this.dirty = true; + } + + private void redraw(GraphView view) { + float dHalf = thickness * 0.5f; + + int n = xs.length * 4; // each point has 2 offset vertices and each vertex has an x and y component + this.vertexBuffer = new float[n]; + int vertexIndex = 0; + + // first only calculate the start point vertices + // they are dependent on the first and second point + float x0 = view.g2sX(xs[0]); + float y0 = view.g2sY(ys[0]); + float x1 = view.g2sX(xs[1]); + float y1 = view.g2sY(ys[1]); + // last pos + float lx = x0; + float ly = y0; + + float dx = x1 - x0; + float dy = y1 - y0; + float len = MathUtils.sqrt(dx * dx + dy * dy); + if (len == 0) throw new IllegalArgumentException("Graph can't handle the same point back to back!"); + dx /= len; + dy /= len; + // perpendicular + float px = -dy; + float py = dx; + // last perpendicular thickness offset + float lpox = px * dHalf; + float lpoy = py * dHalf; + + vertexIndex = storePoints(vertexIndex, view, lx, ly, lpox, lpoy); + + // calculate all points except start and endpoint + // these depend on both their neighbors + for (int i = 1; i < xs.length - 1; i++) { + x0 = view.g2sX(xs[i]); + y0 = view.g2sY(ys[i]); + x1 = view.g2sX(xs[i + 1]); + y1 = view.g2sY(ys[i + 1]); + + dx = x1 - x0; + dy = y1 - y0; + len = MathUtils.sqrt(dx * dx + dy * dy); + if (len == 0) continue; + dx /= len; + dy /= len; + // perpendicular + px = -dy; + py = dx; + // perpendicular thickness offset + float pox = px * dHalf; + float poy = py * dHalf; + float ox, oy; + if (pox == lpox && poy == lpoy) { // linear + ox = pox; + oy = poy; + } else { + // get the average offset of this and the last point and of this and the next point + ox = Interpolations.lerp(lpox, pox, 0.5f); + oy = Interpolations.lerp(lpoy, poy, 0.5f); + // normalize it + len = MathUtils.sqrt(ox * ox + oy * oy); + ox /= len; + oy /= len; + // angle between now offset vector and last perpendicular offset vector + float cosAngle = (ox * lpox + oy * lpoy) / (1 * dHalf); + // calc hypotenuse and use it to calculate the actual length of the offset vector + float hypotenuse = this.thickness / cosAngle; + ox *= hypotenuse * 0.5f; + oy *= hypotenuse * 0.5f; + } + + vertexIndex = storePoints(vertexIndex, view, x0, y0, ox, oy); + + lx = x0; + ly = y0; + lpox = pox; + lpoy = poy; + } + + // finally calculate endpoint + // this depends on itself and the point before + int last = this.xs.length - 1; + x0 = lx; + y0 = ly; + x1 = view.g2sX(xs[last]); + y1 = view.g2sY(ys[last]); + + dx = x1 - x0; + dy = y1 - y0; + len = MathUtils.sqrt(dx * dx + dy * dy); + if (len == 0) throw new IllegalArgumentException("Graph can't handle the same point back to back!"); + dx /= len; + dy /= len; + // perpendicular + px = -dy; + py = dx; + // last perpendicular thickness offset + lpox = px * dHalf; + lpoy = py * dHalf; + + storePoints(vertexIndex, view, x1, y1, lpox, lpoy); + } + + private int storePoints(int index, GraphView view, float sx, float sy, float ox, float oy) { + this.vertexBuffer[index++] = sx - ox; + this.vertexBuffer[index++] = sy - oy; + this.vertexBuffer[index++] = sx + ox; + this.vertexBuffer[index++] = sy + oy; + return index; + } + + public void draw(GraphView view) { + if (xs.length == 0) return; + if (xs.length == 1) { + GuiDraw.drawRect(xs[0] - thickness / 2, ys[0] - thickness / 2, thickness, thickness, color); + return; + } + if (this.dirty) { + redraw(view); + this.dirty = false; + } + int r = Color.getRed(color); + int g = Color.getGreen(color); + int b = Color.getBlue(color); + int a = Color.getAlpha(color); + Platform.setupDrawColor(); + Platform.startDrawing(Platform.DrawMode.TRIANGLE_STRIP, Platform.VertexFormat.POS_COLOR, buffer -> { + for (int i = 0; i < this.vertexBuffer.length; i += 2) { + buffer.pos(this.vertexBuffer[i], this.vertexBuffer[i + 1], 0).color(r, g, b, a).endVertex(); + } + }); + } + + public float getThickness() { + return thickness; + } + + public int getColor() { + return color; + } + + public float[] getX() { + return xs; + } + + public float[] getY() { + return ys; + } + + public float[] getData(GuiAxis axis) { + return axis.isHorizontal() ? this.xs : this.ys; + } + + public Plot data(float[] x, float[] y) { + if (x.length != y.length) throw new IllegalArgumentException("X and Y must have the same length!"); + this.xs = x; + this.ys = y; + redraw(); + return this; + } + + public Plot thickness(float thickness) { + this.thickness = thickness; + redraw(); + return this; + } + + public Plot color(int color) { + this.color = color; + return this; + } +} diff --git a/src/main/java/com/cleanroommc/modularui/test/TestGuis.java b/src/main/java/com/cleanroommc/modularui/test/TestGuis.java index a79d6806e..ae094b3a0 100644 --- a/src/main/java/com/cleanroommc/modularui/test/TestGuis.java +++ b/src/main/java/com/cleanroommc/modularui/test/TestGuis.java @@ -533,14 +533,13 @@ public static ModularPanel buildCollapseDisabledChildrenUI() { float[] x = FloatArrayMath.linspace(-5, 5, 100); float[] y = FloatArrayMath.sin(x, null); return new ModularPanel("graph") - .size(200, 150) + .size(200, 200) .padding(5) .overlay(new GraphDrawable() .yLim(-1.2f, 1.2f) - .autoXLim() .xTickFinder(MathUtils.PI_HALF, 1) .yTickFinder(0.2f, 1) - .data(x, y)); + .plot(x, y)); } private static class TestPanel extends ModularPanel { From 4dcf5e850cbaaa499d96161feeb707bf4982eb92 Mon Sep 17 00:00:00 2001 From: brachy84 Date: Tue, 9 Dec 2025 15:42:33 +0100 Subject: [PATCH 22/50] graph aspect ratio (cherry picked from commit b480bcc416a9784c770186d5282f507742f41a3e) --- .../modularui/drawable/graph/GraphAxis.java | 1 - .../drawable/graph/GraphDrawable.java | 5 +++ .../modularui/drawable/graph/GraphView.java | 31 +++++++++++++------ 3 files changed, 27 insertions(+), 10 deletions(-) diff --git a/src/main/java/com/cleanroommc/modularui/drawable/graph/GraphAxis.java b/src/main/java/com/cleanroommc/modularui/drawable/graph/GraphAxis.java index 67c5b2cfa..e969abd89 100644 --- a/src/main/java/com/cleanroommc/modularui/drawable/graph/GraphAxis.java +++ b/src/main/java/com/cleanroommc/modularui/drawable/graph/GraphAxis.java @@ -102,7 +102,6 @@ void applyPadding(GraphView graphView) { off += textRenderer.getFontHeight() + AXIS_LABEL_OFFSET; } graphView.sx0 += off; - graphView.sx1 -= off; } } diff --git a/src/main/java/com/cleanroommc/modularui/drawable/graph/GraphDrawable.java b/src/main/java/com/cleanroommc/modularui/drawable/graph/GraphDrawable.java index bf0676bb7..07e4e6547 100644 --- a/src/main/java/com/cleanroommc/modularui/drawable/graph/GraphDrawable.java +++ b/src/main/java/com/cleanroommc/modularui/drawable/graph/GraphDrawable.java @@ -252,4 +252,9 @@ public GraphDrawable minorGridLineColor(int color) { this.minorGridLineColor = color; return this; } + + public GraphDrawable graphAspectRatio(float aspectRatio) { + this.view.setAspectRatio(aspectRatio); + return this; + } } diff --git a/src/main/java/com/cleanroommc/modularui/drawable/graph/GraphView.java b/src/main/java/com/cleanroommc/modularui/drawable/graph/GraphView.java index ca06f17e3..ae56cd911 100644 --- a/src/main/java/com/cleanroommc/modularui/drawable/graph/GraphView.java +++ b/src/main/java/com/cleanroommc/modularui/drawable/graph/GraphView.java @@ -7,6 +7,7 @@ @ApiStatus.Experimental public class GraphView { + float aspectRatio = 0; // screen rectangle float sx0, sy0, sx1, sy1; // graph rectangle @@ -15,15 +16,19 @@ public class GraphView { float zeroX, zeroY; void postResize() { - float w = sx1 - sx0, h = sy1 - sy0; - if (w > h) { - float d = w - h; - sx0 += d / 2; - sx1 -= d / 2; - } else if (h > w) { - float d = h - w; - sy0 += d / 2; - sy1 -= d / 2; + if (this.aspectRatio > 0) { + float w = sx1 - sx0, h = sy1 - sy0; + float properW = this.aspectRatio * h; + if (w > properW) { + float d = w - properW; + sx0 += d / 2; + sx1 -= d / 2; + } else if (w < properW) { + float properH = w / this.aspectRatio; + float d = h - properH; + sy0 += d / 2; + sy1 -= d / 2; + } } this.zeroX = g2sX(0); this.zeroY = g2sY(0); @@ -93,4 +98,12 @@ public float getZeroX() { public float getZeroY() { return zeroY; } + + public void setAspectRatio(float aspectRatio) { + this.aspectRatio = aspectRatio; + } + + public float getAspectRatio() { + return aspectRatio; + } } From 9f977f34a3f54f87b8ffb7eef15a1a798f70d59f Mon Sep 17 00:00:00 2001 From: brachy84 Date: Tue, 9 Dec 2025 16:23:00 +0100 Subject: [PATCH 23/50] aspectRatio for Icon (cherry picked from commit b94603e21af0ce35df2eaf566646afbb73341268) --- .../cleanroommc/modularui/drawable/Icon.java | 44 ++++++++++++++++--- .../cleanroommc/modularui/test/TestGuis.java | 23 ++++++++++ 2 files changed, 61 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/cleanroommc/modularui/drawable/Icon.java b/src/main/java/com/cleanroommc/modularui/drawable/Icon.java index 6bc5bef70..af57ba169 100644 --- a/src/main/java/com/cleanroommc/modularui/drawable/Icon.java +++ b/src/main/java/com/cleanroommc/modularui/drawable/Icon.java @@ -21,6 +21,7 @@ public class Icon implements IIcon, IJsonSerializable { private final IDrawable drawable; private int width = 0, height = 0; + private float aspectRatio = 0; private Alignment alignment = Alignment.Center; private final Box margin = new Box(); @@ -55,13 +56,37 @@ public void draw(GuiContext context, int x, int y, int width, int height, Widget y += this.margin.getTop(); width -= this.margin.horizontal(); height -= this.margin.vertical(); - if (this.width > 0) { - x += (int) (width * this.alignment.x - this.width * this.alignment.x); - width = this.width; + int frameWidth = width; + int frameHeight = height; + if (this.width > 0) width = this.width; + if (this.height > 0) height = this.height; + if (this.aspectRatio > 0) { + if (this.width <= 0) { + if (this.height <= 0) { + // width and height is unset, so adjust width or height so that one of them takes the full space + float w = width, h = height; + float properW = this.aspectRatio * h; + if (w > properW) { + width = (int) properW; + } else if (w < properW) { + height = (int) (w / this.aspectRatio); + } + } else { + // height is set, so adjust width to height + float properW = this.aspectRatio * height; + width = (int) properW; + } + } else if (this.height <= 0) { + // width is set, so adjust height to width + height = (int) (width / this.aspectRatio); + } } - if (this.height > 0) { - y += (int) (height * this.alignment.y - this.height * this.alignment.y); - height = this.height; + // apply alignment + if (width != frameWidth) { + x += (int) (frameWidth * this.alignment.x - width * this.alignment.x); + } + if (height != frameHeight) { + y += (int) (frameHeight * this.alignment.y - height * this.alignment.y); } this.drawable.draw(context, x, y, width, height, widgetTheme); } @@ -96,6 +121,11 @@ public Icon size(int size) { return width(size).height(size); } + public Icon aspectRatio(float aspectRatio) { + this.aspectRatio = aspectRatio; + return this; + } + public Icon alignment(Alignment alignment) { this.alignment = alignment; return this; @@ -148,6 +178,7 @@ public void loadFromJson(JsonObject json) { this.height = (json.has("autoHeight") || json.has("autoSize")) && JsonHelper.getBoolean(json, true, "autoHeight", "autoSize") ? 0 : JsonHelper.getInt(json, 0, "height", "h", "size"); + this.aspectRatio = JsonHelper.getFloat(json, 0, "aspectRatio"); this.alignment = JsonHelper.deserialize(json, Alignment.class, Alignment.Center, "alignment", "align"); this.margin.fromJson(json); } @@ -161,6 +192,7 @@ public boolean saveToJson(JsonObject json) { json.add("drawable", JsonHelper.serialize(this.drawable)); json.addProperty("width", this.width); json.addProperty("height", this.height); + json.addProperty("aspectRatio", this.aspectRatio); json.add("alignment", JsonHelper.serialize(this.alignment)); this.margin.toJson(json); return true; diff --git a/src/main/java/com/cleanroommc/modularui/test/TestGuis.java b/src/main/java/com/cleanroommc/modularui/test/TestGuis.java index ae094b3a0..227404f0a 100644 --- a/src/main/java/com/cleanroommc/modularui/test/TestGuis.java +++ b/src/main/java/com/cleanroommc/modularui/test/TestGuis.java @@ -539,9 +539,32 @@ public static ModularPanel buildCollapseDisabledChildrenUI() { .yLim(-1.2f, 1.2f) .xTickFinder(MathUtils.PI_HALF, 1) .yTickFinder(0.2f, 1) + .graphAspectRatio(16 / 9f) .plot(x, y)); } + public static @NotNull ModularPanel buildAspectRatioUI() { + return new ModularPanel("aspect_ratio") + .coverChildren() + .padding(10) + .child(new Row() + .childPadding(10) + .coverChildren() + .child(new Rectangle().color(Color.BLUE_ACCENT.main) + .asIcon().aspectRatio(4f / 3) + .asWidget().size(80) + .overlay(IKey.str("4:3 Free"))) + .child(new Rectangle().color(Color.RED_ACCENT.main) + .asIcon().aspectRatio(4f / 3).width(70) + .asWidget().size(80) + .overlay(IKey.str("4:3 | width = 70"))) + .child(new Rectangle().color(Color.LIGHT_GREEN.main) + .asIcon().aspectRatio(4f / 3).height(45).alignment(Alignment.BottomRight) + .asWidget().size(80) + .overlay(IKey.str("4:3 | height = 45\nBottom Right")))) + .overlay(); + } + private static class TestPanel extends ModularPanel { public TestPanel(String name) { From 857a546a7111639dc9762ba4690a77c6e6514c97 Mon Sep 17 00:00:00 2001 From: brachy84 Date: Tue, 9 Dec 2025 16:28:27 +0100 Subject: [PATCH 24/50] warn when aspect ratio cant be applied (cherry picked from commit f9305bf08ecc83709008859ded52083270d0dfa7) --- src/main/java/com/cleanroommc/modularui/drawable/Icon.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/main/java/com/cleanroommc/modularui/drawable/Icon.java b/src/main/java/com/cleanroommc/modularui/drawable/Icon.java index af57ba169..b3338074d 100644 --- a/src/main/java/com/cleanroommc/modularui/drawable/Icon.java +++ b/src/main/java/com/cleanroommc/modularui/drawable/Icon.java @@ -1,5 +1,6 @@ package com.cleanroommc.modularui.drawable; +import com.cleanroommc.modularui.ModularUI; import com.cleanroommc.modularui.api.IJsonSerializable; import com.cleanroommc.modularui.api.drawable.IDrawable; import com.cleanroommc.modularui.api.drawable.IIcon; @@ -9,6 +10,7 @@ import com.cleanroommc.modularui.utils.JsonHelper; import com.cleanroommc.modularui.widget.sizer.Box; +import net.minecraftforge.fml.relauncher.FMLLaunchHandler; import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; @@ -79,6 +81,10 @@ public void draw(GuiContext context, int x, int y, int width, int height, Widget } else if (this.height <= 0) { // width is set, so adjust height to width height = (int) (width / this.aspectRatio); + } else if (FMLLaunchHandler.isDeobfuscatedEnvironment()) { + ModularUI.LOGGER.error("Aspect ration in Icon can't be applied when width and height are specified"); + // remove aspect ratio to avoid log spamming, it does nothing in the current state anyway + this.aspectRatio = 0; } } // apply alignment From 60173fbae7689ee9b7a377f213e475b2a819140f Mon Sep 17 00:00:00 2001 From: brachy84 Date: Tue, 9 Dec 2025 17:54:05 +0100 Subject: [PATCH 25/50] multiple default colors & apply stencil to plot (cherry picked from commit 80320229f7cae8a3bb3e65de2166bb4d532d13ba) --- .../drawable/graph/GraphDrawable.java | 14 +++++- .../modularui/drawable/graph/GraphView.java | 48 +++++++++++++++++++ .../modularui/drawable/graph/Plot.java | 15 +++++- 3 files changed, 75 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/cleanroommc/modularui/drawable/graph/GraphDrawable.java b/src/main/java/com/cleanroommc/modularui/drawable/graph/GraphDrawable.java index 07e4e6547..1b1186391 100644 --- a/src/main/java/com/cleanroommc/modularui/drawable/graph/GraphDrawable.java +++ b/src/main/java/com/cleanroommc/modularui/drawable/graph/GraphDrawable.java @@ -3,6 +3,7 @@ import com.cleanroommc.modularui.api.GuiAxis; import com.cleanroommc.modularui.api.drawable.IDrawable; import com.cleanroommc.modularui.drawable.GuiDraw; +import com.cleanroommc.modularui.drawable.Stencil; import com.cleanroommc.modularui.screen.viewport.GuiContext; import com.cleanroommc.modularui.theme.WidgetTheme; import com.cleanroommc.modularui.utils.Color; @@ -46,13 +47,15 @@ public void draw(GuiContext context, int x, int y, int width, int height, Widget this.view.postResize(); } if (this.backgroundColor != 0) { - GuiDraw.drawRect(this.view.sx0, this.view.sy0, this.view.sx1 - this.view.sx0, this.view.sy1 - this.view.sy0, this.backgroundColor); + GuiDraw.drawRect(this.view.sx0, this.view.sy0, this.view.getScreenWidth(), this.view.getScreenHeight(), this.backgroundColor); } Platform.setupDrawColor(); drawGrid(context); + Stencil.applyTransformed((int) this.view.sx0, (int) this.view.sy0, (int) (this.view.getScreenWidth() + 1), (int) (this.view.getScreenHeight() + 1)); for (Plot plot : this.plots) { plot.draw(this.view); } + Stencil.remove(); drawTicks(context); GuiDraw.drawBorderOutsideLTRB(this.view.sx0, this.view.sy0, this.view.sx1, this.view.sy1, 0.5f, Color.BLACK.main); this.x.drawLabels(this.view, this.y); @@ -95,6 +98,15 @@ private boolean compute() { this.dirty = false; this.x.compute(this.plots); this.y.compute(this.plots); + int colorIndex = 0; + for (Plot plot : this.plots) { + if (plot.defaultColor) { + plot.color = Plot.DEFAULT_PLOT_COLORS[colorIndex]; + if (++colorIndex == Plot.DEFAULT_PLOT_COLORS.length) { + colorIndex = 0; + } + } + } return true; } diff --git a/src/main/java/com/cleanroommc/modularui/drawable/graph/GraphView.java b/src/main/java/com/cleanroommc/modularui/drawable/graph/GraphView.java index ae56cd911..bdfa444d7 100644 --- a/src/main/java/com/cleanroommc/modularui/drawable/graph/GraphView.java +++ b/src/main/java/com/cleanroommc/modularui/drawable/graph/GraphView.java @@ -106,4 +106,52 @@ public void setAspectRatio(float aspectRatio) { public float getAspectRatio() { return aspectRatio; } + + public float getGraphX0() { + return gx0; + } + + public float getGraphX1() { + return gx1; + } + + public float getGraphY0() { + return gy0; + } + + public float getGraphY1() { + return gy1; + } + + public float getScreenX0() { + return sx0; + } + + public float getScreenX1() { + return sx1; + } + + public float getScreenY0() { + return sy0; + } + + public float getScreenY1() { + return sy1; + } + + public float getScreenWidth() { + return sx1 - sx0; + } + + public float getScreenHeight() { + return sy1 - sy0; + } + + public float getGraphWidth() { + return gx1 - gx0; + } + + public float getGraphHeight() { + return gy1 - gy0; + } } diff --git a/src/main/java/com/cleanroommc/modularui/drawable/graph/Plot.java b/src/main/java/com/cleanroommc/modularui/drawable/graph/Plot.java index 348743a00..ab82d9b10 100644 --- a/src/main/java/com/cleanroommc/modularui/drawable/graph/Plot.java +++ b/src/main/java/com/cleanroommc/modularui/drawable/graph/Plot.java @@ -10,10 +10,22 @@ public class Plot { + public static final int[] DEFAULT_PLOT_COLORS = { + Color.BLUE_ACCENT.main, + Color.ORANGE_ACCENT.darker(0), + Color.GREEN.main, + Color.RED.main, + Color.DEEP_PURPLE_ACCENT.main, + Color.BROWN.main, + Color.TEAL.main, + Color.LIME.main + }; + float[] xs = FloatArrayMath.EMPTY; float[] ys = FloatArrayMath.EMPTY; float thickness = 1f; - int color = Color.BLUE_ACCENT.main; + boolean defaultColor = true; + int color; private float[] vertexBuffer; private boolean dirty = true; @@ -192,6 +204,7 @@ public Plot thickness(float thickness) { public Plot color(int color) { this.color = color; + this.defaultColor = color == 0; return this; } } From 622910512b068a1a32a6f81fdb03ec3a3688967f Mon Sep 17 00:00:00 2001 From: brachy84 Date: Tue, 9 Dec 2025 18:08:40 +0100 Subject: [PATCH 26/50] float array math (cherry picked from commit fd78b39354c74483a43a3670bb555c12fe3de2ff) --- .../cleanroommc/modularui/test/TestGuis.java | 13 +++--- .../modularui/utils/FloatArrayMath.java | 43 +++++++++++++------ 2 files changed, 35 insertions(+), 21 deletions(-) diff --git a/src/main/java/com/cleanroommc/modularui/test/TestGuis.java b/src/main/java/com/cleanroommc/modularui/test/TestGuis.java index 227404f0a..7e03248bf 100644 --- a/src/main/java/com/cleanroommc/modularui/test/TestGuis.java +++ b/src/main/java/com/cleanroommc/modularui/test/TestGuis.java @@ -31,7 +31,6 @@ import com.cleanroommc.modularui.utils.FloatArrayMath; import com.cleanroommc.modularui.utils.Interpolation; import com.cleanroommc.modularui.utils.Interpolations; -import com.cleanroommc.modularui.utils.MathUtils; import com.cleanroommc.modularui.utils.Platform; import com.cleanroommc.modularui.utils.fakeworld.ArraySchema; import com.cleanroommc.modularui.utils.fakeworld.FakeEntity; @@ -530,17 +529,15 @@ public static ModularPanel buildCollapseDisabledChildrenUI() { } public static @NotNull ModularPanel buildGraphUI() { - float[] x = FloatArrayMath.linspace(-5, 5, 100); - float[] y = FloatArrayMath.sin(x, null); + float[] x = FloatArrayMath.linspace(-25, 25, 200); + // sin(x) / x + float[] y1 = FloatArrayMath.div(FloatArrayMath.sin(x, null), x, null); return new ModularPanel("graph") - .size(200, 200) + .size(200, 160) .padding(5) .overlay(new GraphDrawable() - .yLim(-1.2f, 1.2f) - .xTickFinder(MathUtils.PI_HALF, 1) - .yTickFinder(0.2f, 1) .graphAspectRatio(16 / 9f) - .plot(x, y)); + .plot(x, y1)); } public static @NotNull ModularPanel buildAspectRatioUI() { diff --git a/src/main/java/com/cleanroommc/modularui/utils/FloatArrayMath.java b/src/main/java/com/cleanroommc/modularui/utils/FloatArrayMath.java index a89237a91..347c9592b 100644 --- a/src/main/java/com/cleanroommc/modularui/utils/FloatArrayMath.java +++ b/src/main/java/com/cleanroommc/modularui/utils/FloatArrayMath.java @@ -171,10 +171,7 @@ public static float min(float[] arr) { * @return result array */ public static float[] plus(float[] src, float op, float @Nullable [] res) { - if (res == null) res = new float[src.length]; - int n = Math.min(src.length, res.length); - for (int i = 0; i < n; i++) res[i] = src[i] + op; - return res; + return applyEach(src, v -> v + op, res); } /** @@ -186,11 +183,7 @@ public static float[] plus(float[] src, float op, float @Nullable [] res) { * @return result array */ public static float[] plus(float[] src, float[] op, float @Nullable [] res) { - if (src.length != op.length) throw new IllegalArgumentException("Can't add arrays of different size."); - if (res == null) res = new float[src.length]; - int n = Math.min(src.length, res.length); - for (int i = 0; i < n; i++) res[i] = src[i] + op[i]; - return res; + return applyEach(src, op, Float::sum, res); } /** @@ -202,16 +195,21 @@ public static float[] plus(float[] src, float[] op, float @Nullable [] res) { * @return result array */ public static float[] mult(float[] src, float op, float @Nullable [] res) { - if (res == null) res = new float[src.length]; - int n = Math.min(src.length, res.length); - for (int i = 0; i < n; i++) res[i] = src[i] * op; - return res; + return applyEach(src, v -> v * op, res); + } + + public static float[] mult(float[] src, float[] op, float @Nullable [] res) { + return applyEach(src, op, (v, op1) -> v * op1, res); } public static float[] div(float[] src, float op, float @Nullable [] res) { return mult(src, 1 / op, res); } + public static float[] div(float[] src, float[] op, float @Nullable [] res) { + return applyEach(src, op, (v, op1) -> v / op1, res); + } + public static float[] diff(float[] src) { if (src.length < 2) return EMPTY; if (src.length == 2) return new float[]{src[1] - src[0]}; @@ -247,6 +245,16 @@ public static float[] applyEach(float[] src, float[] operands1, float[] operands return res; } + public static float[] applyEach(float[] src, float[][] operands, NFloatOperator op, float @Nullable [] res) { + if (src.length != operands.length) { + throw new IllegalArgumentException("Can't apply operator to operands of different size."); + } + if (res == null) res = new float[src.length]; + int n = Math.min(src.length, res.length); + for (int i = 0; i < n; i++) res[i] = op.apply(src[i], operands[i]); + return res; + } + public static float[] abs(float[] src, float @Nullable [] res) { return applyEach(src, Math::abs, res); } @@ -259,6 +267,10 @@ public static float[] cos(float[] src, float @Nullable [] res) { return applyEach(src, MathUtils::cos, res); } + public static float[] tan(float[] src, float @Nullable [] res) { + return applyEach(src, MathUtils::tan, res); + } + public static float[] clamp(float[] src, float min, float max, float @Nullable [] res) { return applyEach(src, v -> MathUtils.clamp(v, min, max), res); } @@ -291,4 +303,9 @@ public interface TernaryFloatOperator { float apply(float v, float op1, float op2); } + + public interface NFloatOperator { + + float apply(float v, float[] op); + } } From 865261bf2c0ca1ca84cdd3b55a990f596f54a2cc Mon Sep 17 00:00:00 2001 From: brachy84 Date: Wed, 10 Dec 2025 10:26:53 +0100 Subject: [PATCH 27/50] fallback to sync hypervisor (cherry picked from commit 20e9a1ca6e7dcf30926b5585f59754db40dd0f17) --- src/main/java/com/cleanroommc/modularui/widget/Widget.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/cleanroommc/modularui/widget/Widget.java b/src/main/java/com/cleanroommc/modularui/widget/Widget.java index 734eec676..0362c2613 100644 --- a/src/main/java/com/cleanroommc/modularui/widget/Widget.java +++ b/src/main/java/com/cleanroommc/modularui/widget/Widget.java @@ -144,13 +144,16 @@ public void afterInit() {} /** * Retrieves, verifies, and initialises a linked sync handler. - * Custom logic should be handled in {@link #isValidSyncHandler(SyncHandler)}. + * Custom logic should be handled in {@link #setSyncOrValue(ISyncOrValue)}. */ @Override public void initialiseSyncHandler(ModularSyncManager syncManager, boolean late) { SyncHandler handler = this.syncHandler; if (handler == null && this.syncKey != null) { handler = syncManager.getSyncHandler(getPanel().getName(), this.syncKey); + if (handler == null && !syncManager.getMainPSM().getPanelName().equals(getPanel().getName())) { + handler = syncManager.getMainPSM().getSyncHandlerFromMapKey(this.syncKey); + } } if (handler != null) setSyncOrValue(handler); if (this.syncHandler instanceof ValueSyncHandler valueSyncHandler && valueSyncHandler.getChangeListener() == null) { From 90eee69cce22a49c28cf2a67901c96dc5ef10288 Mon Sep 17 00:00:00 2001 From: brachy84 Date: Wed, 10 Dec 2025 16:46:47 +0100 Subject: [PATCH 28/50] use double for plot data points & more math (cherry picked from commit fcc15a7a83a311be6b917177043205d2d4311684) --- .../drawable/graph/AutoMajorTickFinder.java | 18 +- .../drawable/graph/AutoMinorTickFinder.java | 10 +- .../modularui/drawable/graph/GraphAxis.java | 64 +-- .../drawable/graph/GraphDrawable.java | 10 +- .../modularui/drawable/graph/GraphView.java | 44 +- .../drawable/graph/MajorTickFinder.java | 2 +- .../drawable/graph/MinorTickFinder.java | 2 +- .../modularui/drawable/graph/Plot.java | 26 +- .../cleanroommc/modularui/test/TestGuis.java | 6 +- .../com/cleanroommc/modularui/utils/DAM.java | 428 ++++++++++++++++++ .../utils/{FloatArrayMath.java => FAM.java} | 119 ++++- .../modularui/utils/MathUtils.java | 11 + 12 files changed, 651 insertions(+), 89 deletions(-) create mode 100644 src/main/java/com/cleanroommc/modularui/utils/DAM.java rename src/main/java/com/cleanroommc/modularui/utils/{FloatArrayMath.java => FAM.java} (73%) diff --git a/src/main/java/com/cleanroommc/modularui/drawable/graph/AutoMajorTickFinder.java b/src/main/java/com/cleanroommc/modularui/drawable/graph/AutoMajorTickFinder.java index 9105a1be7..232d92d9a 100644 --- a/src/main/java/com/cleanroommc/modularui/drawable/graph/AutoMajorTickFinder.java +++ b/src/main/java/com/cleanroommc/modularui/drawable/graph/AutoMajorTickFinder.java @@ -6,7 +6,7 @@ public class AutoMajorTickFinder implements MajorTickFinder { private final boolean autoAdjust; - private float multiple = 10; + private double multiple = 10; public AutoMajorTickFinder(boolean autoAdjust) { this.autoAdjust = autoAdjust; @@ -18,10 +18,10 @@ public AutoMajorTickFinder(float multiple) { } @Override - public float[] find(float min, float max, float[] ticks) { + public double[] find(double min, double max, double[] ticks) { int s = (int) Math.ceil((max - min) / multiple) + 2; - if (s > ticks.length) ticks = new float[s]; - float next = (float) (Math.floor(min / multiple) * multiple); + if (s > ticks.length) ticks = new double[s]; + double next = Math.floor(min / multiple) * multiple; for (int i = 0; i < s; i++) { ticks[i] = next; if (next > max) { @@ -34,17 +34,17 @@ public float[] find(float min, float max, float[] ticks) { return ticks; } - void calculateAutoTickMultiple(float min, float max) { - float step = (max - min) / 5; + void calculateAutoTickMultiple(double min, double max) { + double step = (max - min) / 5; if (step < 1) { int significantPlaces = (int) Math.abs(Math.log10(step)) + 2; - float ten = (float) Math.pow(10, significantPlaces); + double ten = Math.pow(10, significantPlaces); step = (int) (step * ten + 0.2f) / ten; } else if (step == 1) { step = 0.2f; } else { int significantPlaces = (int) Math.log10(step) - 1; - float ten = (float) Math.pow(10, significantPlaces); + double ten = Math.pow(10, significantPlaces); step = (int) (step / ten + 0.2f) * ten; } setMultiple(step); @@ -54,7 +54,7 @@ public boolean isAutoAdjust() { return autoAdjust; } - public void setMultiple(float multiple) { + public void setMultiple(double multiple) { this.multiple = multiple; } } diff --git a/src/main/java/com/cleanroommc/modularui/drawable/graph/AutoMinorTickFinder.java b/src/main/java/com/cleanroommc/modularui/drawable/graph/AutoMinorTickFinder.java index c1c678adf..29a5464c9 100644 --- a/src/main/java/com/cleanroommc/modularui/drawable/graph/AutoMinorTickFinder.java +++ b/src/main/java/com/cleanroommc/modularui/drawable/graph/AutoMinorTickFinder.java @@ -12,14 +12,14 @@ public AutoMinorTickFinder(int amountBetweenMajors) { } @Override - public float[] find(float min, float max, float[] majorTicks, float[] ticks) { + public double[] find(double min, double max, double[] majorTicks, double[] ticks) { int s = majorTicks.length * this.amountBetweenMajors; - if (ticks.length < s) ticks = new float[s]; + if (ticks.length < s) ticks = new double[s]; int k = 0; for (int i = 0; i < majorTicks.length - 1; i++) { - if (Float.isNaN(majorTicks[i + 1])) break; - float next = majorTicks[i]; - float d = (majorTicks[i + 1] - next) / (amountBetweenMajors + 1); + if (Double.isNaN(majorTicks[i + 1])) break; + double next = majorTicks[i]; + double d = (majorTicks[i + 1] - next) / (amountBetweenMajors + 1); for (int j = 0; j < amountBetweenMajors; j++) { next += d; if (next >= min) ticks[k++] = next; diff --git a/src/main/java/com/cleanroommc/modularui/drawable/graph/GraphAxis.java b/src/main/java/com/cleanroommc/modularui/drawable/graph/GraphAxis.java index e969abd89..a1433b3ca 100644 --- a/src/main/java/com/cleanroommc/modularui/drawable/graph/GraphAxis.java +++ b/src/main/java/com/cleanroommc/modularui/drawable/graph/GraphAxis.java @@ -4,7 +4,7 @@ import com.cleanroommc.modularui.drawable.GuiDraw; import com.cleanroommc.modularui.drawable.text.TextRenderer; import com.cleanroommc.modularui.utils.Alignment; -import com.cleanroommc.modularui.utils.FloatArrayMath; +import com.cleanroommc.modularui.utils.DAM; import net.minecraft.client.renderer.BufferBuilder; @@ -24,14 +24,14 @@ public class GraphAxis { public final GuiAxis axis; - public float[] majorTicks = new float[8]; - public float[] minorTicks = new float[16]; + public double[] majorTicks = new double[8]; + public double[] minorTicks = new double[16]; public TextRenderer.Line[] tickLabels = new TextRenderer.Line[8]; private float maxLabelWidth = 0; public MajorTickFinder majorTickFinder = new AutoMajorTickFinder(true); public MinorTickFinder minorTickFinder = new AutoMinorTickFinder(2); public String label; - public float min, max; + public double min, max; public boolean autoLimits = true; public GraphAxis(GuiAxis axis) { @@ -44,21 +44,21 @@ void compute(List plots) { this.min = 0; this.max = 0; } else if (plots.size() == 1) { - this.min = FloatArrayMath.min(plots.get(0).getData(this.axis)); - this.max = FloatArrayMath.max(plots.get(0).getData(this.axis)); + this.min = DAM.min(plots.get(0).getData(this.axis)); + this.max = DAM.max(plots.get(0).getData(this.axis)); } else { - float min = Float.MAX_VALUE, max = Float.MIN_VALUE; + double min = Double.MAX_VALUE, max = Double.MIN_VALUE; for (Plot plot : plots) { - float m = FloatArrayMath.min(plot.getData(this.axis)); + double m = DAM.min(plot.getData(this.axis)); if (m < min) min = m; - m = FloatArrayMath.max(plot.getData(this.axis)); + m = DAM.max(plot.getData(this.axis)); if (m > max) max = m; } this.min = min; this.max = max; } if (this.axis.isVertical()) { - float padding = (this.max - this.min) * 0.05f; + double padding = (this.max - this.min) * 0.05f; this.max += padding; this.min -= padding; } @@ -74,12 +74,12 @@ void compute(List plots) { } textRenderer.setScale(TICK_LABEL_SCALE); this.maxLabelWidth = 0; - float maxDiff = FloatArrayMath.max(FloatArrayMath.diff(this.majorTicks)); + double maxDiff = DAM.max(DAM.diff(this.majorTicks)); int significantPlaces = (int) Math.abs(Math.log10(maxDiff)) + 2; DecimalFormat format = new DecimalFormat(); format.setMaximumFractionDigits(significantPlaces); for (int i = 0; i < this.tickLabels.length; i++) { - if (Float.isNaN(this.majorTicks[i])) break; + if (Double.isNaN(this.majorTicks[i])) break; this.tickLabels[i] = textRenderer.line(format.format(this.majorTicks[i])); if (this.tickLabels[i].getWidth() > this.maxLabelWidth) { this.maxLabelWidth = this.tickLabels[i].getWidth(); @@ -106,7 +106,7 @@ void applyPadding(GraphView graphView) { } void drawGridLines(BufferBuilder buffer, GraphView view, GraphAxis other, boolean major, float d, int r, int g, int b, int a) { - float[] pos = major ? this.majorTicks : this.minorTicks; + double[] pos = major ? this.majorTicks : this.minorTicks; float dHalf = d / 2; if (axis.isHorizontal()) { float otherMin = view.g2sY(other.max); @@ -120,7 +120,7 @@ void drawGridLines(BufferBuilder buffer, GraphView view, GraphAxis other, boolea } void drawTicks(BufferBuilder buffer, GraphView view, GraphAxis other, boolean major, float thickness, float length, int r, int g, int b, int a) { - float[] pos = major ? this.majorTicks : this.minorTicks; + double[] pos = major ? this.majorTicks : this.minorTicks; float dHalf = thickness / 2; if (axis.isHorizontal()) { float otherMin = view.g2sY(other.min); @@ -131,30 +131,30 @@ void drawTicks(BufferBuilder buffer, GraphView view, GraphAxis other, boolean ma } } - private void drawLinesOnHorizontal(BufferBuilder buffer, GraphView view, float[] pos, float dHalf, + private void drawLinesOnHorizontal(BufferBuilder buffer, GraphView view, double[] pos, float dHalf, float crossLow, float crossHigh, int r, int g, int b, int a) { - for (float p : pos) { - if (Float.isNaN(p)) break; + for (double p : pos) { + if (Double.isNaN(p)) break; if (p < min || p > max) continue; - p = view.g2sX(p); + float fp = view.g2sX(p); - float x0 = p - dHalf; - float x1 = p + dHalf; + float x0 = fp - dHalf; + float x1 = fp + dHalf; GuiDraw.drawRectRaw(buffer, x0, crossLow, x1, crossHigh, r, g, b, a); } } - private void drawLinesOnVertical(BufferBuilder buffer, GraphView view, float[] pos, float dHalf, + private void drawLinesOnVertical(BufferBuilder buffer, GraphView view, double[] pos, float dHalf, float crossLow, float crossHigh, int r, int g, int b, int a) { - for (float p : pos) { - if (Float.isNaN(p)) break; + for (double p : pos) { + if (Double.isNaN(p)) break; if (p < min || p > max) continue; - p = view.g2sY(p); + float fp = view.g2sY(p); - float y0 = p - dHalf; - float y1 = p + dHalf; + float y0 = fp - dHalf; + float y1 = fp + dHalf; GuiDraw.drawRectRaw(buffer, crossLow, y0, crossHigh, y1, r, g, b, a); } } @@ -166,8 +166,8 @@ void drawLabels(GraphView view, GraphAxis other) { textRenderer.setAlignment(Alignment.TopCenter, 100); float y = view.g2sY(other.min) + TICK_LABEL_OFFSET; for (int i = 0; i < this.majorTicks.length; i++) { - float pos = this.majorTicks[i]; - if (Float.isNaN(pos)) break; + double pos = this.majorTicks[i]; + if (Double.isNaN(pos)) break; if (pos < min || pos > max) continue; textRenderer.setPos((int) (view.g2sX(pos) - 50), (int) y); textRenderer.draw(this.tickLabels[i].getText()); @@ -177,8 +177,8 @@ void drawLabels(GraphView view, GraphAxis other) { textRenderer.setAlignment(Alignment.CenterRight, this.maxLabelWidth, 20); float x = view.g2sX(other.min) - TICK_LABEL_OFFSET - this.maxLabelWidth; for (int i = 0; i < this.majorTicks.length; i++) { - float pos = this.majorTicks[i]; - if (Float.isNaN(pos)) break; + double pos = this.majorTicks[i]; + if (Double.isNaN(pos)) break; if (pos < min || pos > max) continue; textRenderer.setPos((int) x, (int) (view.g2sY(pos) - 10)); textRenderer.draw(this.tickLabels[i].getText()); @@ -190,11 +190,11 @@ public GuiAxis getAxis() { return axis; } - public float getMax() { + public double getMax() { return max; } - public float getMin() { + public double getMin() { return min; } diff --git a/src/main/java/com/cleanroommc/modularui/drawable/graph/GraphDrawable.java b/src/main/java/com/cleanroommc/modularui/drawable/graph/GraphDrawable.java index 1b1186391..e5027e6f8 100644 --- a/src/main/java/com/cleanroommc/modularui/drawable/graph/GraphDrawable.java +++ b/src/main/java/com/cleanroommc/modularui/drawable/graph/GraphDrawable.java @@ -21,7 +21,6 @@ public class GraphDrawable implements IDrawable { //private IDrawable background; private int backgroundColor = Color.WHITE.main; - private boolean borderTop = true, borderLeft = true, borderBottom = false, borderRight = false; private float majorTickThickness = 1f, majorTickLength = 3f, minorTickThickness = 0.5f, minorTickLength = 1.5f; private float gridLineWidth = 0.5f; private int gridLineColor = Color.withAlpha(Color.BLACK.main, 0.4f); @@ -188,23 +187,23 @@ public GraphDrawable backgroundColor(int color) { return this; } - public GraphDrawable plot(float[] x, float[] y) { + public GraphDrawable plot(double[] x, double[] y) { return plot(new Plot().data(x, y)); } - public GraphDrawable plot(float[] x, float[] y, int color) { + public GraphDrawable plot(double[] x, double[] y, int color) { return plot(new Plot() .data(x, y) .color(color)); } - public GraphDrawable plot(float[] x, float[] y, float thickness) { + public GraphDrawable plot(double[] x, double[] y, float thickness) { return plot(new Plot() .data(x, y) .thickness(thickness)); } - public GraphDrawable plot(float[] x, float[] y, float thickness, int color) { + public GraphDrawable plot(double[] x, double[] y, float thickness, int color) { return plot(new Plot() .data(x, y) .thickness(thickness) @@ -213,6 +212,7 @@ public GraphDrawable plot(float[] x, float[] y, float thickness, int color) { public GraphDrawable plot(Plot plot) { this.plots.add(plot); + plot.redraw(); return this; } diff --git a/src/main/java/com/cleanroommc/modularui/drawable/graph/GraphView.java b/src/main/java/com/cleanroommc/modularui/drawable/graph/GraphView.java index bdfa444d7..58ce810b7 100644 --- a/src/main/java/com/cleanroommc/modularui/drawable/graph/GraphView.java +++ b/src/main/java/com/cleanroommc/modularui/drawable/graph/GraphView.java @@ -1,17 +1,15 @@ package com.cleanroommc.modularui.drawable.graph; -import com.cleanroommc.modularui.utils.Interpolations; - import org.jetbrains.annotations.ApiStatus; @ApiStatus.Experimental public class GraphView { float aspectRatio = 0; - // screen rectangle + // screen rectangle (float since range is usually in order 1e3) float sx0, sy0, sx1, sy1; - // graph rectangle - float gx0, gy0, gx1, gy1; + // graph rectangle (double since range and accuracy can be anything) + double gx0, gy0, gx1, gy1; float zeroX, zeroY; @@ -45,7 +43,7 @@ boolean setScreen(float x0, float y0, float x1, float y1) { return false; } - void setGraph(float x0, float y0, float x1, float y1) { + void setGraph(double x0, double y0, double x1, double y1) { this.gx0 = x0; this.gy0 = y0; this.gx1 = x1; @@ -54,40 +52,40 @@ void setGraph(float x0, float y0, float x1, float y1) { this.zeroY = g2sY(0); } - public float g2sX(float v) { - return transform(v, gx0, gx1, sx0, sx1); + public float g2sX(double v) { + return (float) transform(v, gx0, gx1, sx0, sx1); } - public float g2sY(float v) { + public float g2sY(double v) { // gy0 and gy1 inverted on purpose // screen y0 is top, graph y0 is bottom - return transform(v, gy1, gy0, sy0, sy1); + return (float) transform(v, gy1, gy0, sy0, sy1); } - public float g2sScaleX() { + public double g2sScaleX() { return scale(gx0, gx1, sx0, sx1); } - public float g2sScaleY() { + public double g2sScaleY() { return scale(gy1, gy0, sy0, sy1); } - public float s2gX(float v) { + public double s2gX(float v) { return transform(v, sx0, sx1, gx0, gx1); } - public float s2gY(float v) { + public double s2gY(float v) { // gy0 and gy1 inverted on purpose // screen y0 is top, graph y0 is bottom return transform(v, sy0, sy1, gy1, gy0); } - private float transform(float v, float fromMin, float fromMax, float toMin, float toMax) { + private double transform(double v, double fromMin, double fromMax, double toMin, double toMax) { v = (v - fromMin) / (fromMax - fromMin); // reverse lerp - return Interpolations.lerp(toMin, toMax, v); + return toMin + (toMax - toMin) * v; } - private float scale(float fromMin, float fromMax, float toMin, float toMax) { + private double scale(double fromMin, double fromMax, double toMin, double toMax) { return (toMax - toMin) / (fromMax - fromMin); } @@ -107,19 +105,19 @@ public float getAspectRatio() { return aspectRatio; } - public float getGraphX0() { + public double getGraphX0() { return gx0; } - public float getGraphX1() { + public double getGraphX1() { return gx1; } - public float getGraphY0() { + public double getGraphY0() { return gy0; } - public float getGraphY1() { + public double getGraphY1() { return gy1; } @@ -147,11 +145,11 @@ public float getScreenHeight() { return sy1 - sy0; } - public float getGraphWidth() { + public double getGraphWidth() { return gx1 - gx0; } - public float getGraphHeight() { + public double getGraphHeight() { return gy1 - gy0; } } diff --git a/src/main/java/com/cleanroommc/modularui/drawable/graph/MajorTickFinder.java b/src/main/java/com/cleanroommc/modularui/drawable/graph/MajorTickFinder.java index 741d18d97..6c04470a6 100644 --- a/src/main/java/com/cleanroommc/modularui/drawable/graph/MajorTickFinder.java +++ b/src/main/java/com/cleanroommc/modularui/drawable/graph/MajorTickFinder.java @@ -5,5 +5,5 @@ @ApiStatus.Experimental public interface MajorTickFinder { - float[] find(float min, float max, float[] ticks); + double[] find(double min, double max, double[] ticks); } diff --git a/src/main/java/com/cleanroommc/modularui/drawable/graph/MinorTickFinder.java b/src/main/java/com/cleanroommc/modularui/drawable/graph/MinorTickFinder.java index a892be4a4..ba97c9f63 100644 --- a/src/main/java/com/cleanroommc/modularui/drawable/graph/MinorTickFinder.java +++ b/src/main/java/com/cleanroommc/modularui/drawable/graph/MinorTickFinder.java @@ -5,5 +5,5 @@ @ApiStatus.Experimental public interface MinorTickFinder { - float[] find(float min, float max, float[] majorTicks, float[] ticks); + double[] find(double min, double max, double[] majorTicks, double[] ticks); } diff --git a/src/main/java/com/cleanroommc/modularui/drawable/graph/Plot.java b/src/main/java/com/cleanroommc/modularui/drawable/graph/Plot.java index ab82d9b10..a622fbfbf 100644 --- a/src/main/java/com/cleanroommc/modularui/drawable/graph/Plot.java +++ b/src/main/java/com/cleanroommc/modularui/drawable/graph/Plot.java @@ -1,11 +1,13 @@ package com.cleanroommc.modularui.drawable.graph; +import com.cleanroommc.modularui.ModularUI; import com.cleanroommc.modularui.api.GuiAxis; import com.cleanroommc.modularui.drawable.GuiDraw; import com.cleanroommc.modularui.utils.Color; -import com.cleanroommc.modularui.utils.FloatArrayMath; +import com.cleanroommc.modularui.utils.DAM; import com.cleanroommc.modularui.utils.Interpolations; import com.cleanroommc.modularui.utils.MathUtils; +import com.cleanroommc.modularui.utils.NumberFormat; import com.cleanroommc.modularui.utils.Platform; public class Plot { @@ -21,13 +23,13 @@ public class Plot { Color.LIME.main }; - float[] xs = FloatArrayMath.EMPTY; - float[] ys = FloatArrayMath.EMPTY; + double[] xs = DAM.EMPTY; + double[] ys = DAM.EMPTY; float thickness = 1f; boolean defaultColor = true; int color; - private float[] vertexBuffer; + private float[] vertexBuffer; // screen coords need to be way less accurate than graph coords, so float is fine private boolean dirty = true; public void redraw() { @@ -35,6 +37,7 @@ public void redraw() { } private void redraw(GraphView view) { + long time = System.nanoTime(); float dHalf = thickness * 0.5f; int n = xs.length * 4; // each point has 2 offset vertices and each vertex has an x and y component @@ -136,6 +139,8 @@ private void redraw(GraphView view) { lpoy = py * dHalf; storePoints(vertexIndex, view, x1, y1, lpox, lpoy); + time = System.nanoTime() - time; + ModularUI.LOGGER.error("Calculating vertices from {} data points took {}s", xs.length, NumberFormat.formatNanos(time)); } private int storePoints(int index, GraphView view, float sx, float sy, float ox, float oy) { @@ -149,7 +154,7 @@ private int storePoints(int index, GraphView view, float sx, float sy, float ox, public void draw(GraphView view) { if (xs.length == 0) return; if (xs.length == 1) { - GuiDraw.drawRect(xs[0] - thickness / 2, ys[0] - thickness / 2, thickness, thickness, color); + GuiDraw.drawRect(view.g2sX(xs[0]) - thickness / 2, view.g2sY(ys[0]) - thickness / 2, thickness, thickness, color); return; } if (this.dirty) { @@ -162,9 +167,12 @@ public void draw(GraphView view) { int a = Color.getAlpha(color); Platform.setupDrawColor(); Platform.startDrawing(Platform.DrawMode.TRIANGLE_STRIP, Platform.VertexFormat.POS_COLOR, buffer -> { + long time = System.nanoTime(); for (int i = 0; i < this.vertexBuffer.length; i += 2) { buffer.pos(this.vertexBuffer[i], this.vertexBuffer[i + 1], 0).color(r, g, b, a).endVertex(); } + time = System.nanoTime() - time; + ModularUI.LOGGER.error("Drawing plot with {} points took {}s", xs.length, NumberFormat.formatNanos(time)); }); } @@ -176,19 +184,19 @@ public int getColor() { return color; } - public float[] getX() { + public double[] getX() { return xs; } - public float[] getY() { + public double[] getY() { return ys; } - public float[] getData(GuiAxis axis) { + public double[] getData(GuiAxis axis) { return axis.isHorizontal() ? this.xs : this.ys; } - public Plot data(float[] x, float[] y) { + public Plot data(double[] x, double[] y) { if (x.length != y.length) throw new IllegalArgumentException("X and Y must have the same length!"); this.xs = x; this.ys = y; diff --git a/src/main/java/com/cleanroommc/modularui/test/TestGuis.java b/src/main/java/com/cleanroommc/modularui/test/TestGuis.java index 7e03248bf..6ba9408f2 100644 --- a/src/main/java/com/cleanroommc/modularui/test/TestGuis.java +++ b/src/main/java/com/cleanroommc/modularui/test/TestGuis.java @@ -28,7 +28,7 @@ import com.cleanroommc.modularui.utils.Color; import com.cleanroommc.modularui.utils.GlStateManager; import com.cleanroommc.modularui.utils.ColorShade; -import com.cleanroommc.modularui.utils.FloatArrayMath; +import com.cleanroommc.modularui.utils.DAM; import com.cleanroommc.modularui.utils.Interpolation; import com.cleanroommc.modularui.utils.Interpolations; import com.cleanroommc.modularui.utils.Platform; @@ -529,9 +529,9 @@ public static ModularPanel buildCollapseDisabledChildrenUI() { } public static @NotNull ModularPanel buildGraphUI() { - float[] x = FloatArrayMath.linspace(-25, 25, 200); + double[] x = DAM.linspace(-25, 25, 200); // sin(x) / x - float[] y1 = FloatArrayMath.div(FloatArrayMath.sin(x, null), x, null); + double[] y1 = DAM.div(DAM.sin(x, null), x, null); return new ModularPanel("graph") .size(200, 160) .padding(5) diff --git a/src/main/java/com/cleanroommc/modularui/utils/DAM.java b/src/main/java/com/cleanroommc/modularui/utils/DAM.java new file mode 100644 index 000000000..10ac73448 --- /dev/null +++ b/src/main/java/com/cleanroommc/modularui/utils/DAM.java @@ -0,0 +1,428 @@ +package com.cleanroommc.modularui.utils; + +import org.jetbrains.annotations.Nullable; + +import java.util.Arrays; + +/** + * A helper class providing math operations on 1D double arrays similar to numpy. + * DAM stands for DoubleArrayMath. + */ +public class DAM { + + public static final double[] EMPTY = new double[0]; + + /** + * Creates an array of length n filled with zeros. + * + * @param n length + * @return zero filled array + */ + public static double[] zeros(int n) { + return new double[n]; + } + + /** + * Creates an array of length n filled with f. + * + * @param n length + * @param f fill value + * @return f filled array + */ + public static double[] full(int n, double f) { + double[] arr = new double[n]; + Arrays.fill(arr, f); + return arr; + } + + + /** + * Creates an array of length n filled with ones. + * + * @param n length + * @return one filled array + */ + public static double[] ones(int n) { + return full(n, 1); + } + + public static double[] copyInto(double[] src, double @Nullable [] res) { + if (src == res) return res; + if (res == null) res = new double[src.length]; + int n = Math.min(src.length, res.length); + System.arraycopy(src, 0, res, 0, n); + return res; + } + + public static double[] subArray(double[] src, int start, int length) { + double[] res = new double[length]; + System.arraycopy(src, start, res, 0, length); + return res; + } + + public static double[] ofFloats(float[] src) { + double[] res = new double[src.length]; + for (int i = 0; i < src.length; i++) res[i] = src[i]; + return res; + } + + public static double[] ofInts(int[] src) { + double[] res = new double[src.length]; + for (int i = 0; i < src.length; i++) res[i] = src[i]; + return res; + } + + public static double[] ofLongs(long[] src) { + double[] res = new double[src.length]; + for (int i = 0; i < src.length; i++) res[i] = src[i]; + return res; + } + + public static double[] linspace(double start, double stop) { + return linspace(start, stop, 50, true); + } + + public static double[] linspace(double start, double stop, boolean includeEndpoint) { + return linspace(start, stop, 50, includeEndpoint); + } + + public static double[] linspace(double start, double stop, int n) { + return linspace(start, stop, n, true); + } + + /** + * Creates an evenly spaced array over a specified interval. + * + * @param start start of interval + * @param stop stop of interval + * @param n sample size = array length + * @param includeEndpoint true if stop should be included at the end of the array + * @return evenly spaced array over interval + */ + public static double[] linspace(double start, double stop, int n, boolean includeEndpoint) { + double[] arr = new double[n]; + double step = (stop - start) / (includeEndpoint ? n + 1 : n); + int s = n; + if (includeEndpoint) { + arr[n - 1] = stop; + s--; + } + for (int i = 0; i < s; i++) { + arr[i] = start + step * i; + } + return arr; + } + + public static double[] arange(double stop, double step) { + return arange(0, stop, step); + } + + public static double[] arange(double start, double stop, double step) { + double[] arr = new double[(int) Math.ceil((stop - start) / step)]; + for (int i = 0; i < arr.length; i++) { + arr[i] = start + step * i; + } + return arr; + } + + /** + * Returns the index of the largest value in the array. If array is empty, -1 is returned. + * + * @param arr array + * @return index of largest value + */ + public static int argMax(double[] arr) { + if (arr.length == 0) return -1; + if (arr.length == 1) return 0; + if (arr.length == 2) return arr[0] >= arr[1] ? 0 : 1; + int index = 0; + for (int i = 1; i < arr.length; i++) { + if (arr[i] > arr[index]) index = i; + } + return index; + } + + /** + * Returns the index of the smallest value in the array. If array is empty, -1 is returned. + * + * @param arr array + * @return index of smallest value + */ + public static int argMin(double[] arr) { + if (arr.length == 0) return -1; + if (arr.length == 1) return 0; + if (arr.length == 2) return arr[0] <= arr[1] ? 0 : 1; + int index = 0; + for (int i = 1; i < arr.length; i++) { + if (arr[i] < arr[index]) index = i; + } + return index; + } + + /** + * Returns the largest value in the array. If array is empty, 0 is returned. + * + * @param arr array + * @return largest value + */ + public static double max(double[] arr) { + int i = argMax(arr); + return i < 0 ? 0 : arr[i]; + } + + /** + * Returns the smallest value in the array. If array is empty, 0 is returned. + * + * @param arr array + * @return smallest value + */ + public static double min(double[] arr) { + int i = argMin(arr); + return i < 0 ? 0 : arr[i]; + } + + /** + * Adds an operand to every element of the source array. + * + * @param src source array + * @param op operand + * @param res result array. If this is null a new array is created. This can be the same as src. + * @return result array + */ + public static double[] plus(double[] src, double op, double @Nullable [] res) { + return applyEach(src, v -> v + op, res); + } + + public static double[] plusMut(double[] src, double op) { + return plus(src, op, src); + } + + /** + * Adds each element of the operand to the element at the corresponding index of the source array. + * + * @param src source array + * @param op operands + * @param res result array. If this is null a new array is created. This can be the same as src. + * @return result array + */ + public static double[] plus(double[] src, double[] op, double @Nullable [] res) { + return applyEach(src, op, Double::sum, res); + } + + public static double[] plusMut(double[] src, double[] op) { + return plus(src, op, src); + } + + /** + * Multiplies an operand with every element of the source array. + * + * @param src source array + * @param op operand + * @param res result array. If this is null a new array is created. This can be the same as src. + * @return result array + */ + public static double[] mult(double[] src, double op, double @Nullable [] res) { + return applyEach(src, v -> v * op, res); + } + + public static double[] multMut(double[] src, double op) { + return mult(src, op, src); + } + + public static double[] mult(double[] src, double[] op, double @Nullable [] res) { + return applyEach(src, op, (v, op1) -> v * op1, res); + } + + public static double[] multMut(double[] src, double[] op) { + return mult(src, op, src); + } + + public static double[] div(double[] src, double op, double @Nullable [] res) { + return mult(src, 1 / op, res); + } + + public static double[] divMut(double[] src, double op) { + return div(src, op, src); + } + + public static double[] div(double[] src, double[] op, double @Nullable [] res) { + return applyEach(src, op, (v, op1) -> v / op1, res); + } + + public static double[] divMut(double[] src, double[] op) { + return div(src, op, src); + } + + public static double[] reciprocal(double a, double[] b, double @Nullable [] res) { + return applyEach(b, v -> a / v, res); + } + + public static double[] square(double[] src, double @Nullable [] res) { + return applyEach(src, v -> v * v, res); + } + + public static double[] cube(double[] src, double @Nullable [] res) { + return applyEach(src, v -> v * v * v, res); + } + + public static double[] pow(double[] src, double op, double @Nullable [] res) { + return applyEach(src, v -> (double) Math.pow(v, op), res); + } + + public static double[] diff(double[] src) { + if (src.length < 2) return EMPTY; + if (src.length == 2) return new double[]{src[1] - src[0]}; + double[] res = new double[src.length - 1]; + for (int i = 0; i < res.length; i++) { + res[i] = src[i + 1] - src[i]; + } + return res; + } + + public static double[] applyEach(double[] src, UnaryDoubleOperator op, double @Nullable [] res) { + if (res == null) res = new double[src.length]; + int n = Math.min(src.length, res.length); + for (int i = 0; i < n; i++) res[i] = op.apply(src[i]); + return res; + } + + public static double[] applyEach(double[] src, double[] operands, BinaryDoubleOperator op, double @Nullable [] res) { + if (src.length != operands.length) throw new IllegalArgumentException("Can't apply operator to operands of different size."); + if (res == null) res = new double[src.length]; + int n = Math.min(src.length, res.length); + for (int i = 0; i < n; i++) res[i] = op.apply(src[i], operands[i]); + return res; + } + + public static double[] applyEach(double[] src, double[] operands1, double[] operands2, TernaryDoubleOperator op, double @Nullable [] res) { + if (src.length != operands1.length || src.length != operands2.length) { + throw new IllegalArgumentException("Can't apply operator to operands of different size."); + } + if (res == null) res = new double[src.length]; + int n = Math.min(src.length, res.length); + for (int i = 0; i < n; i++) res[i] = op.apply(src[i], operands1[i], operands2[i]); + return res; + } + + public static double[] applyEach(double[] src, double[][] operands, NDoubleOperator op, double @Nullable [] res) { + if (src.length != operands.length) { + throw new IllegalArgumentException("Can't apply operator to operands of different size."); + } + if (res == null) res = new double[src.length]; + int n = Math.min(src.length, res.length); + for (int i = 0; i < n; i++) res[i] = op.apply(src[i], operands[i]); + return res; + } + + public static double[] abs(double[] src, double @Nullable [] res) { + return applyEach(src, Math::abs, res); + } + + public static double[] sin(double[] src, double @Nullable [] res) { + return applyEach(src, Math::sin, res); + } + + public static double[] cos(double[] src, double @Nullable [] res) { + return applyEach(src, Math::cos, res); + } + + public static double[] tan(double[] src, double @Nullable [] res) { + return applyEach(src, Math::tan, res); + } + + public static double[] clamp(double[] src, double min, double max, double @Nullable [] res) { + return applyEach(src, v -> MathUtils.clamp(v, min, max), res); + } + + public static double[] polynomial(double[] src, double[] coeff, double @Nullable [] res) { + if (coeff.length == 0) return copyInto(src, res); + if (coeff.length == 1) return plus(src, coeff[0], res); + return applyEach(src, x -> { + double y = 0; + y += coeff[0]; + if (coeff.length == 2) return y + x * coeff[1]; + for (int i = 1; i < coeff.length; i++) { + y += (double) (Math.pow(x, i) * coeff[i]); + } + return y; + }, res); + } + + public static double reduce(double[] src, BinaryDoubleOperator op) { + if (src.length == 0) return 0; + if (src.length == 1) return src[0]; + double res = op.apply(src[0], src[1]); + if (src.length == 2) return res; + for (int i = 2; i < src.length; i++) { + res = op.apply(res, src[i]); + } + return res; + } + + public static double[] reverse(double[] src, double @Nullable [] res) { + if (res == null) res = new double[src.length]; + for (int i = 0; i < src.length; i++) { + res[i] = src[src.length - i - 1]; + } + return res; + } + + public static double sum(double[] src) { + return reduce(src, Double::sum); + } + + public static double product(double[] src) { + return reduce(src, (f1, f2) -> f1 * f2); + } + + public static double arithmeticMean(double[] src) { + return sum(src) / src.length; + } + + public static double geometricMean(double[] src) { + return (double) Math.pow(product(src), 1f / src.length); + } + + public static double[] concat(double[] a, double[] b) { + double[] res = new double[a.length + b.length]; + System.arraycopy(a, 0, res, 0, a.length); + System.arraycopy(b, 0, res, a.length, b.length); + return res; + } + + public static double[] flatten(double[]... src) { + if (src.length == 0) return EMPTY; + if (src.length == 1) return src[0]; + if (src.length == 2) return concat(src[0], src[1]); + int n = 0; + for (double[] doubles : src) n += doubles.length; + if (n == 0) return EMPTY; + double[] res = new double[n]; + n = 0; + for (double[] doubles : src) { + System.arraycopy(doubles, 0, res, n, doubles.length); + n += doubles.length; + } + return res; + } + + public interface UnaryDoubleOperator { + + double apply(double v); + } + + public interface BinaryDoubleOperator { + + double apply(double v, double op); + } + + public interface TernaryDoubleOperator { + + double apply(double v, double op1, double op2); + } + + public interface NDoubleOperator { + + double apply(double v, double[] op); + } +} diff --git a/src/main/java/com/cleanroommc/modularui/utils/FloatArrayMath.java b/src/main/java/com/cleanroommc/modularui/utils/FAM.java similarity index 73% rename from src/main/java/com/cleanroommc/modularui/utils/FloatArrayMath.java rename to src/main/java/com/cleanroommc/modularui/utils/FAM.java index 347c9592b..e202cc555 100644 --- a/src/main/java/com/cleanroommc/modularui/utils/FloatArrayMath.java +++ b/src/main/java/com/cleanroommc/modularui/utils/FAM.java @@ -6,8 +6,9 @@ /** * A helper class providing math operations on 1D float arrays similar to numpy. + * FAM stands for FloatArrayMath. */ -public class FloatArrayMath { +public class FAM { public static final float[] EMPTY = new float[0]; @@ -59,6 +60,24 @@ public static float[] subArray(float[] src, int start, int length) { return res; } + public static float[] ofDoubles(double[] src) { + float[] res = new float[src.length]; + for (int i = 0; i < src.length; i++) res[i] = (float) src[i]; + return res; + } + + public static float[] ofInts(int[] src) { + float[] res = new float[src.length]; + for (int i = 0; i < src.length; i++) res[i] = src[i]; + return res; + } + + public static float[] ofLongs(long[] src) { + float[] res = new float[src.length]; + for (int i = 0; i < src.length; i++) res[i] = src[i]; + return res; + } + public static float[] linspace(float start, float stop) { return linspace(start, stop, 50, true); } @@ -174,6 +193,10 @@ public static float[] plus(float[] src, float op, float @Nullable [] res) { return applyEach(src, v -> v + op, res); } + public static float[] plusMut(float[] src, float op) { + return plus(src, op, src); + } + /** * Adds each element of the operand to the element at the corresponding index of the source array. * @@ -186,6 +209,10 @@ public static float[] plus(float[] src, float[] op, float @Nullable [] res) { return applyEach(src, op, Float::sum, res); } + public static float[] plusMut(float[] src, float[] op) { + return plus(src, op, src); + } + /** * Multiplies an operand with every element of the source array. * @@ -198,18 +225,50 @@ public static float[] mult(float[] src, float op, float @Nullable [] res) { return applyEach(src, v -> v * op, res); } + public static float[] multMut(float[] src, float op) { + return mult(src, op, src); + } + public static float[] mult(float[] src, float[] op, float @Nullable [] res) { return applyEach(src, op, (v, op1) -> v * op1, res); } + public static float[] multMut(float[] src, float[] op) { + return mult(src, op, src); + } + public static float[] div(float[] src, float op, float @Nullable [] res) { return mult(src, 1 / op, res); } + public static float[] divMut(float[] src, float op) { + return div(src, op, src); + } + public static float[] div(float[] src, float[] op, float @Nullable [] res) { return applyEach(src, op, (v, op1) -> v / op1, res); } + public static float[] divMut(float[] src, float[] op) { + return div(src, op, src); + } + + public static float[] reciprocal(float a, float[] b, float @Nullable [] res) { + return applyEach(b, v -> a / v, res); + } + + public static float[] square(float[] src, float @Nullable [] res) { + return applyEach(src, v -> v * v, res); + } + + public static float[] cube(float[] src, float @Nullable [] res) { + return applyEach(src, v -> v * v * v, res); + } + + public static float[] pow(float[] src, float op, float @Nullable [] res) { + return applyEach(src, v -> (float) Math.pow(v, op), res); + } + public static float[] diff(float[] src) { if (src.length < 2) return EMPTY; if (src.length == 2) return new float[]{src[1] - src[0]}; @@ -289,6 +348,64 @@ public static float[] polynomial(float[] src, float[] coeff, float @Nullable [] }, res); } + public static float reduce(float[] src, BinaryFloatOperator op) { + if (src.length == 0) return 0; + if (src.length == 1) return src[0]; + float res = op.apply(src[0], src[1]); + if (src.length == 2) return res; + for (int i = 2; i < src.length; i++) { + res = op.apply(res, src[i]); + } + return res; + } + + public static float[] reverse(float[] src, float @Nullable [] res) { + if (res == null) res = new float[src.length]; + for (int i = 0; i < src.length; i++) { + res[i] = src[src.length - i - 1]; + } + return res; + } + + public static float sum(float[] src) { + return reduce(src, Float::sum); + } + + public static float product(float[] src) { + return reduce(src, (f1, f2) -> f1 * f2); + } + + public static float arithmeticMean(float[] src) { + return sum(src) / src.length; + } + + public static float geometricMean(float[] src) { + return (float) Math.pow(product(src), 1f / src.length); + } + + public static float[] concat(float[] a, float[] b) { + float[] res = new float[a.length + b.length]; + System.arraycopy(a, 0, res, 0, a.length); + System.arraycopy(b, 0, res, a.length, b.length); + return res; + } + + public static float[] flatten(float[]... src) { + if (src.length == 0) return EMPTY; + if (src.length == 1) return src[0]; + if (src.length == 2) return concat(src[0], src[1]); + int n = 0; + for (float[] floats : src) n += floats.length; + if (n == 0) return EMPTY; + float[] res = new float[n]; + n = 0; + for (float[] floats : src) { + System.arraycopy(floats, 0, res, n, floats.length); + n += floats.length; + } + return res; + } + public interface UnaryFloatOperator { float apply(float v); diff --git a/src/main/java/com/cleanroommc/modularui/utils/MathUtils.java b/src/main/java/com/cleanroommc/modularui/utils/MathUtils.java index dcc634871..2d327592a 100644 --- a/src/main/java/com/cleanroommc/modularui/utils/MathUtils.java +++ b/src/main/java/com/cleanroommc/modularui/utils/MathUtils.java @@ -187,4 +187,15 @@ public static double sqrt(double v) { public static float sqrt(float v) { return (float) Math.sqrt(v); } + + public static float arithmeticGeometricMean(float a, float b) { + return arithmeticGeometricMean(a, b, 5); + } + + public static float arithmeticGeometricMean(float a, float b, int iterations) { + a = (a + b) / 2; + b = sqrt(a * b); + if (--iterations == 0) return a; + return arithmeticGeometricMean(a, b, iterations); + } } From 8be3ca0b64f7e674e5bdbb971cb5e2bbdc8850d9 Mon Sep 17 00:00:00 2001 From: Zorbatron <46525467+Zorbatron@users.noreply.github.com> Date: Thu, 11 Dec 2025 17:16:43 -0500 Subject: [PATCH 29/50] Add shortcut method to toggle panel open or closed (#184) (cherry picked from commit 6e2d04f9f9c86e5c3b51e5282d3c58268167bf29) --- .../cleanroommc/modularui/api/IPanelHandler.java | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/main/java/com/cleanroommc/modularui/api/IPanelHandler.java b/src/main/java/com/cleanroommc/modularui/api/IPanelHandler.java index 276c23f19..abe9372be 100644 --- a/src/main/java/com/cleanroommc/modularui/api/IPanelHandler.java +++ b/src/main/java/com/cleanroommc/modularui/api/IPanelHandler.java @@ -61,6 +61,21 @@ static IPanelHandler simple(ModularPanel parent, SecondaryPanel.IPanelBuilder pr @ApiStatus.OverrideOnly void closePanelInternal(); + /** + * Toggles this panel open or closed. Delegates to {@link #openPanel()} and {@link #closePanel()}. + * + * @return {@code true} if the panel was opened, {@code false} if it was closed + */ + default boolean togglePanel() { + if (isPanelOpen()) { + closePanel(); + return false; + } else { + openPanel(); + return true; + } + } + /** * Deletes the current cached panel. Should not be used frequently. * This only works on panels which don't have {@link ItemSlotSH} sync handlers. From 03b6e615222b1e2836a0c2d6dd6f973581bac1ec Mon Sep 17 00:00:00 2001 From: brachy84 Date: Sun, 14 Dec 2025 09:29:46 +0100 Subject: [PATCH 30/50] fix re registering sync handlers when already registered to another psm (cherry picked from commit fda3bf7570d22000644f61ed53114a9d15b3da51) --- .../value/sync/DynamicSyncHandler.java | 2 +- .../modularui/value/sync/ISyncRegistrar.java | 2 ++ .../value/sync/ModularSyncManager.java | 5 +++++ .../modularui/value/sync/PanelSyncManager.java | 18 ++++++++++-------- .../modularui/value/sync/SyncHandler.java | 4 ++++ .../modularui/widget/WidgetTree.java | 6 +++--- 6 files changed, 25 insertions(+), 12 deletions(-) diff --git a/src/main/java/com/cleanroommc/modularui/value/sync/DynamicSyncHandler.java b/src/main/java/com/cleanroommc/modularui/value/sync/DynamicSyncHandler.java index c5e2e7310..fce50cbea 100644 --- a/src/main/java/com/cleanroommc/modularui/value/sync/DynamicSyncHandler.java +++ b/src/main/java/com/cleanroommc/modularui/value/sync/DynamicSyncHandler.java @@ -55,7 +55,7 @@ private IWidget parseWidget(PacketBuffer buf) { getSyncManager().allowTemporarySyncHandlerRegistration(false); // collects any unregistered sync handlers // since the sync manager is currently locked and we no longer allow bypassing the lock it will crash if it finds any - int unregistered = WidgetTree.countUnregisteredSyncHandlers(getSyncManager(), widget); + int unregistered = WidgetTree.countUnregisteredSyncHandlers(widget); if (unregistered > 0) { throw new IllegalStateException("Widgets created by DynamicSyncHandler can't have implicitly registered sync handlers. All" + "sync handlers must be registered with a variant of 'PanelSyncManager#getOrCreateSyncHandler(...)'."); diff --git a/src/main/java/com/cleanroommc/modularui/value/sync/ISyncRegistrar.java b/src/main/java/com/cleanroommc/modularui/value/sync/ISyncRegistrar.java index 53ee033f1..422714d9a 100644 --- a/src/main/java/com/cleanroommc/modularui/value/sync/ISyncRegistrar.java +++ b/src/main/java/com/cleanroommc/modularui/value/sync/ISyncRegistrar.java @@ -17,6 +17,8 @@ public interface ISyncRegistrar> { + boolean hasSyncHandler(SyncHandler syncHandler); + default S syncValue(String name, SyncHandler syncHandler) { return syncValue(name, 0, syncHandler); } diff --git a/src/main/java/com/cleanroommc/modularui/value/sync/ModularSyncManager.java b/src/main/java/com/cleanroommc/modularui/value/sync/ModularSyncManager.java index d2cf91e0a..2b16df77f 100644 --- a/src/main/java/com/cleanroommc/modularui/value/sync/ModularSyncManager.java +++ b/src/main/java/com/cleanroommc/modularui/value/sync/ModularSyncManager.java @@ -155,6 +155,11 @@ private static boolean isPlayerSlot(Slot slot) { return false; } + @Override + public boolean hasSyncHandler(SyncHandler syncHandler) { + return this.mainPSM.hasSyncHandler(syncHandler); + } + @Override public ModularSyncManager syncValue(String name, int id, SyncHandler syncHandler) { this.mainPSM.syncValue(name, id, syncHandler); diff --git a/src/main/java/com/cleanroommc/modularui/value/sync/PanelSyncManager.java b/src/main/java/com/cleanroommc/modularui/value/sync/PanelSyncManager.java index 4dc7f9402..e5a01d0cb 100644 --- a/src/main/java/com/cleanroommc/modularui/value/sync/PanelSyncManager.java +++ b/src/main/java/com/cleanroommc/modularui/value/sync/PanelSyncManager.java @@ -17,9 +17,10 @@ import cpw.mods.fml.relauncher.Side; import io.netty.buffer.Unpooled; -import it.unimi.dsi.fastutil.objects.Object2ObjectArrayMap; -import it.unimi.dsi.fastutil.objects.Object2ObjectLinkedOpenHashMap; -import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import it.unimi.dsi.fastutil.objects.Object2ReferenceArrayMap; +import it.unimi.dsi.fastutil.objects.Object2ReferenceLinkedOpenHashMap; +import it.unimi.dsi.fastutil.objects.Object2ReferenceOpenHashMap; +import it.unimi.dsi.fastutil.objects.Reference2ObjectOpenHashMap; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -35,11 +36,11 @@ public class PanelSyncManager implements ISyncRegistrar { - private final Map syncHandlers = new Object2ObjectLinkedOpenHashMap<>(); - private final Map slotGroups = new Object2ObjectOpenHashMap<>(); - private final Map reverseSyncHandlers = new Object2ObjectOpenHashMap<>(); - private final Map syncedActions = new Object2ObjectOpenHashMap<>(); - private final Map subPanels = new Object2ObjectArrayMap<>(); + private final Map syncHandlers = new Object2ReferenceLinkedOpenHashMap<>(); + private final Map slotGroups = new Object2ReferenceOpenHashMap<>(); + private final Map reverseSyncHandlers = new Reference2ObjectOpenHashMap<>(); + private final Map syncedActions = new Object2ReferenceOpenHashMap<>(); + private final Map subPanels = new Object2ReferenceArrayMap<>(); private final ModularSyncManager modularSyncManager; private String panelName; private boolean init = true; @@ -166,6 +167,7 @@ public void setCursorItem(ItemStack stack) { getModularSyncManager().setCursorItem(stack); } + @Override public boolean hasSyncHandler(SyncHandler syncHandler) { return syncHandler.isValid() && syncHandler.getSyncManager() == this && this.reverseSyncHandlers.containsKey(syncHandler); } diff --git a/src/main/java/com/cleanroommc/modularui/value/sync/SyncHandler.java b/src/main/java/com/cleanroommc/modularui/value/sync/SyncHandler.java index 649f7592e..7643b5eca 100644 --- a/src/main/java/com/cleanroommc/modularui/value/sync/SyncHandler.java +++ b/src/main/java/com/cleanroommc/modularui/value/sync/SyncHandler.java @@ -178,6 +178,10 @@ public PanelSyncManager getSyncManager() { return this.syncManager; } + public final boolean isRegistered() { + return isValid() && this.syncManager.hasSyncHandler(this); + } + @Override public boolean isSyncHandler() { return true; diff --git a/src/main/java/com/cleanroommc/modularui/widget/WidgetTree.java b/src/main/java/com/cleanroommc/modularui/widget/WidgetTree.java index 6e2a3af08..138e61649 100644 --- a/src/main/java/com/cleanroommc/modularui/widget/WidgetTree.java +++ b/src/main/java/com/cleanroommc/modularui/widget/WidgetTree.java @@ -502,7 +502,7 @@ public static void collectSyncValues(PanelSyncManager syncManager, String panelN String syncKey = ModularSyncManager.AUTO_SYNC_PREFIX + panelName; foreachChildBFS(panel, widget -> { if (widget instanceof ISynced synced) { - if (synced.isSynced() && !syncManager.hasSyncHandler(synced.getSyncHandler())) { + if (synced.isSynced() && !synced.getSyncHandler().isRegistered()) { syncManager.syncValue(syncKey, id.intValue(), synced.getSyncHandler()); id.increment(); } @@ -511,10 +511,10 @@ public static void collectSyncValues(PanelSyncManager syncManager, String panelN }, includePanel); } - public static int countUnregisteredSyncHandlers(PanelSyncManager syncManager, IWidget parent) { + public static int countUnregisteredSyncHandlers(IWidget parent) { MutableInt count = new MutableInt(); foreachChildBFS(parent, widget -> { - if (widget instanceof ISynced synced && synced.isSynced() && !syncManager.hasSyncHandler(synced.getSyncHandler())) { + if (widget instanceof ISynced synced && synced.isSynced() && !synced.getSyncHandler().isRegistered()) { count.increment(); } return true; From ba587283eca6a06c07e2552af38564f8eeb93bd7 Mon Sep 17 00:00:00 2001 From: brachy84 Date: Sun, 14 Dec 2025 13:25:26 +0100 Subject: [PATCH 31/50] ability to register custom IMuiScreen to UISettings (cherry picked from commit c0cf29f2ea733bc72c9e527a9f41f7dd92384403) --- .../modularui/factory/GuiData.java | 4 +-- .../modularui/factory/GuiManager.java | 8 ++--- .../modularui/network/NetworkUtils.java | 5 ++- .../modularui/screen/UISettings.java | 36 ++++++++++++++++++- .../modularui/test/TestGuiContainer.java | 14 ++++++++ .../cleanroommc/modularui/test/TestTile.java | 23 ++++++------ 6 files changed, 69 insertions(+), 21 deletions(-) create mode 100644 src/main/java/com/cleanroommc/modularui/test/TestGuiContainer.java diff --git a/src/main/java/com/cleanroommc/modularui/factory/GuiData.java b/src/main/java/com/cleanroommc/modularui/factory/GuiData.java index a1ab09cdf..5bf0a6551 100644 --- a/src/main/java/com/cleanroommc/modularui/factory/GuiData.java +++ b/src/main/java/com/cleanroommc/modularui/factory/GuiData.java @@ -14,8 +14,8 @@ * This class and subclasses are holding necessary data to find the exact same GUI on client and server. * For example, if the GUI was opened by right-clicking a TileEntity, then this data needs a world and a block pos. *

- * Also see {@link PosGuiData} (useful for TileEntities), {@link SidedPosGuiData} (useful for covers from GregTech) - * for default implementations. + * Also see {@link PosGuiData} (useful for TileEntities), {@link SidedPosGuiData} (useful for covers from GregTech) and + * {@link PlayerInventoryGuiData} (useful for guis opened by interacting with an item in the players inventory) for default implementations. *

*/ public class GuiData { diff --git a/src/main/java/com/cleanroommc/modularui/factory/GuiManager.java b/src/main/java/com/cleanroommc/modularui/factory/GuiManager.java index 27c3bdced..a86ecbc95 100644 --- a/src/main/java/com/cleanroommc/modularui/factory/GuiManager.java +++ b/src/main/java/com/cleanroommc/modularui/factory/GuiManager.java @@ -77,7 +77,7 @@ public static void open(@NotNull UIFactory factory, @NotN PanelSyncManager syncManager = new PanelSyncManager(msm, true); ModularPanel panel = factory.createPanel(guiData, syncManager, settings); WidgetTree.collectSyncValues(syncManager, panel); - ModularContainer container = settings.hasContainer() ? settings.createContainer() : factory.createContainer(); + ModularContainer container = settings.hasCustomContainer() ? settings.createContainer() : factory.createContainer(); container.construct(player, msm, settings, panel.getName(), guiData); // sync to client player.getNextWindowId(); @@ -104,9 +104,9 @@ public static void openFromClient(int windowId, @NotNull UIF WidgetTree.collectSyncValues(syncManager, panel); ModularScreen screen = factory.createScreen(guiData, panel); screen.getContext().setSettings(settings); - ModularContainer container = settings.hasContainer() ? settings.createContainer() : factory.createContainer(); + ModularContainer container = settings.hasCustomContainer() ? settings.createContainer() : factory.createContainer(); container.construct(player, msm, settings, panel.getName(), guiData); - IMuiScreen wrapper = factory.createScreenWrapper(container, screen); + IMuiScreen wrapper = settings.hasCustomGui() ? settings.createGui(container, screen) : factory.createScreenWrapper(container, screen); if (!(wrapper.getGuiScreen() instanceof GuiContainer guiContainer)) { throw new IllegalStateException("The wrapping screen must be a GuiContainer for synced GUIs!"); } @@ -132,7 +132,7 @@ static void openScreen(ModularScreen screen, UISettings settings) { } screen.getContext().setSettings(settings); GuiScreen guiScreen; - if (settings.hasContainer()) { + if (settings.hasCustomContainer()) { ModularContainer container = settings.createContainer(); container.constructClientOnly(); guiScreen = new GuiContainerWrapper(container, screen); diff --git a/src/main/java/com/cleanroommc/modularui/network/NetworkUtils.java b/src/main/java/com/cleanroommc/modularui/network/NetworkUtils.java index ab6e1ff7d..f7e7fb2a4 100644 --- a/src/main/java/com/cleanroommc/modularui/network/NetworkUtils.java +++ b/src/main/java/com/cleanroommc/modularui/network/NetworkUtils.java @@ -20,8 +20,7 @@ public class NetworkUtils { - public static final Consumer EMPTY_PACKET = buffer -> { - }; + public static final Consumer EMPTY_PACKET = buffer -> {}; public static final boolean DEDICATED_CLIENT = FMLCommonHandler.instance().getSide().isClient(); @@ -34,7 +33,7 @@ public static boolean isDedicatedClient() { } public static boolean isClient(EntityPlayer player) { - if (player == null) throw new NullPointerException("Can't get side of null player!"); + if (player == null) return isClient(); return player.worldObj == null ? player instanceof EntityPlayerSP : player.worldObj.isRemote; } diff --git a/src/main/java/com/cleanroommc/modularui/screen/UISettings.java b/src/main/java/com/cleanroommc/modularui/screen/UISettings.java index b39ae33e2..74ec9e43f 100644 --- a/src/main/java/com/cleanroommc/modularui/screen/UISettings.java +++ b/src/main/java/com/cleanroommc/modularui/screen/UISettings.java @@ -1,10 +1,13 @@ package com.cleanroommc.modularui.screen; +import com.cleanroommc.modularui.api.IMuiScreen; import com.cleanroommc.modularui.api.RecipeViewerSettings; import com.cleanroommc.modularui.api.UIFactory; import com.cleanroommc.modularui.factory.GuiData; import com.cleanroommc.modularui.factory.PosGuiData; +import com.cleanroommc.modularui.network.NetworkUtils; +import net.minecraft.client.gui.inventory.GuiContainer; import net.minecraft.entity.player.EntityPlayer; import org.jetbrains.annotations.ApiStatus; @@ -18,6 +21,8 @@ public class UISettings { public static final double DEFAULT_INTERACT_RANGE = 8.0; private Supplier containerSupplier; + @SideOnly(Side.CLIENT) + private GuiCreator guiSupplier; private Predicate canInteractWith; private String theme; private final RecipeViewerSettings recipeViewerSettings; @@ -39,6 +44,19 @@ public void customContainer(Supplier containerSupplier) { this.containerSupplier = containerSupplier; } + /** + * A function for a custom {@link IMuiScreen} implementation. This overrides + * {@link UIFactory#createScreenWrapper(ModularContainer, ModularScreen)}. Note that {@link IMuiScreen#getGuiScreen()} has to be an + * instance of {@link GuiContainer} otherwise an exception is thrown, when the UI opens. + * + * @param guiSupplier a supplier for a gui creator function. It has to be a double function because it crashes on server otherwise. + */ + public void customGui(Supplier guiSupplier) { + if (NetworkUtils.isDedicatedClient()) { + this.guiSupplier = guiSupplier.get(); + } + } + /** * Overrides the default can interact check of {@link UIFactory#canInteractWith(EntityPlayer, GuiData)}. * @@ -91,10 +109,21 @@ public ModularContainer createContainer() { return containerSupplier.get(); } - public boolean hasContainer() { + @ApiStatus.Internal + @SideOnly(Side.CLIENT) + public IMuiScreen createGui(ModularContainer container, ModularScreen screen) { + return guiSupplier.create(container, screen); + } + + public boolean hasCustomContainer() { return containerSupplier != null; } + @SideOnly(Side.CLIENT) + public boolean hasCustomGui() { + return guiSupplier != null; + } + public boolean canPlayerInteractWithUI(EntityPlayer player) { return canInteractWith == null || canInteractWith.test(player); } @@ -102,4 +131,9 @@ public boolean canPlayerInteractWithUI(EntityPlayer player) { public @Nullable String getTheme() { return theme; } + + public interface GuiCreator { + + IMuiScreen create(ModularContainer container, ModularScreen screen); + } } diff --git a/src/main/java/com/cleanroommc/modularui/test/TestGuiContainer.java b/src/main/java/com/cleanroommc/modularui/test/TestGuiContainer.java new file mode 100644 index 000000000..e4728bde3 --- /dev/null +++ b/src/main/java/com/cleanroommc/modularui/test/TestGuiContainer.java @@ -0,0 +1,14 @@ +package com.cleanroommc.modularui.test; + +import com.cleanroommc.modularui.ModularUI; +import com.cleanroommc.modularui.screen.GuiContainerWrapper; +import com.cleanroommc.modularui.screen.ModularContainer; +import com.cleanroommc.modularui.screen.ModularScreen; + +public class TestGuiContainer extends GuiContainerWrapper { + + public TestGuiContainer(ModularContainer container, ModularScreen screen) { + super(container, screen); + ModularUI.LOGGER.info("Created custom gui container"); + } +} diff --git a/src/main/java/com/cleanroommc/modularui/test/TestTile.java b/src/main/java/com/cleanroommc/modularui/test/TestTile.java index 4956eb490..20d283f77 100644 --- a/src/main/java/com/cleanroommc/modularui/test/TestTile.java +++ b/src/main/java/com/cleanroommc/modularui/test/TestTile.java @@ -140,6 +140,8 @@ public int getSlotLimit(int slot) { public ModularPanel buildUI(PosGuiData guiData, PanelSyncManager syncManager, UISettings settings) { final EntityLivingBase fool = new EntityVillager(getWorldObj()); settings.customContainer(() -> new CraftingModularContainer(3, 3, this.craftingInventory)); + settings.customGui(() -> TestGuiContainer::new); + syncManager.addOpenListener(player -> { LOGGER.info("Test Tile panel open by {} on {}", player.getGameProfile().getName(), Thread.currentThread().getName()); }); @@ -264,7 +266,6 @@ public ModularPanel buildUI(PosGuiData guiData, PanelSyncManager syncManager, UI .syncHandler(SyncHandlers.fluidSlot(this.fluidTank))) .child(new ButtonWidget<>() .size(60, 18) - .tooltip(tooltip -> { tooltip.showUpTimer(10); tooltip.addLine(IKey.str("Test Line g")); @@ -288,7 +289,7 @@ public ModularPanel buildUI(PosGuiData guiData, PanelSyncManager syncManager, UI //.flex(flex -> flex.left(3)) // ? .overlay(IKey.str("Button 2"))) .child(new TextFieldWidget() - .tooltip(t->t.addLine("hello, i am overridden!")) + .addTooltipLine("this tooltip is overridden") .size(60, 18) .setTextAlignment(Alignment.Center) .value(SyncHandlers.string(() -> this.value, val -> this.value = val)) @@ -363,15 +364,15 @@ public ModularPanel buildUI(PosGuiData guiData, PanelSyncManager syncManager, UI .childPadding(2) //.child(SlotGroupWidget.playerInventory().left(0)) .child(SlotGroupWidget.builder() - .matrix("III", "III", "III") - .key('I', index -> { - // 4 is the middle slot with a negative priority -> shift click prioritises middle slot - if (index == 4) { - return new ItemSlot().slot(SyncHandlers.itemSlot(this.bigInventory, index).singletonSlotGroup(-100)); - } - return new ItemSlot().slot(SyncHandlers.itemSlot(this.bigInventory, index).slotGroup("item_inv")); - }) - .build().name("9 slot inv") + .matrix("III", "III", "III") + .key('I', index -> { + // 4 is the middle slot with a negative priority -> shift click prioritises middle slot + if (index == 4) { + return new ItemSlot().slot(SyncHandlers.itemSlot(this.bigInventory, index).singletonSlotGroup(-100)); + } + return new ItemSlot().slot(SyncHandlers.itemSlot(this.bigInventory, index).slotGroup("item_inv")); + }) + .build().name("9 slot inv") //.marginBottom(2) ) .child(SlotGroupWidget.builder() From 1f97eb36a2803504afcd4ece938d89acef859c2b Mon Sep 17 00:00:00 2001 From: brachy84 Date: Sun, 14 Dec 2025 15:35:28 +0100 Subject: [PATCH 32/50] null safe DrawableStack (cherry picked from commit 2e03e6b5ce2d46d2cc71fe7737829613e10975ac) --- .../com/cleanroommc/modularui/drawable/DrawableStack.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/cleanroommc/modularui/drawable/DrawableStack.java b/src/main/java/com/cleanroommc/modularui/drawable/DrawableStack.java index 7ea6c601b..5449893f5 100644 --- a/src/main/java/com/cleanroommc/modularui/drawable/DrawableStack.java +++ b/src/main/java/com/cleanroommc/modularui/drawable/DrawableStack.java @@ -27,14 +27,14 @@ public DrawableStack(IDrawable... drawables) { @Override public void draw(GuiContext context, int x, int y, int width, int height, WidgetTheme widgetTheme) { for (IDrawable drawable : this.drawables) { - drawable.draw(context, x, y, width, height, widgetTheme); + if (drawable != null) drawable.draw(context, x, y, width, height, widgetTheme); } } @Override public boolean canApplyTheme() { for (IDrawable drawable : this.drawables) { - if (drawable.canApplyTheme()) { + if (drawable != null && drawable.canApplyTheme()) { return true; } } From b9c55a6a807acdb198426d1a660505da6f017342 Mon Sep 17 00:00:00 2001 From: brachy84 Date: Sun, 14 Dec 2025 16:22:09 +0100 Subject: [PATCH 33/50] port and refactor FluidDisplayWidget, refactor FluidSlot (cherry picked from commit abeb0d3c14715610207446ed2f47a134be8348fb) --- .../modularui/drawable/GuiDraw.java | 49 +++--- .../cleanroommc/modularui/test/TestTile.java | 6 +- .../modularui/utils/MathUtils.java | 10 ++ .../modularui/widget/sizer/Box.java | 3 +- .../widgets/AbstractFluidDisplayWidget.java | 149 ++++++++++++++++++ .../modularui/widgets/FluidDisplayWidget.java | 64 +++----- .../modularui/widgets/slot/FluidSlot.java | 84 ++-------- 7 files changed, 231 insertions(+), 134 deletions(-) create mode 100644 src/main/java/com/cleanroommc/modularui/widgets/AbstractFluidDisplayWidget.java diff --git a/src/main/java/com/cleanroommc/modularui/drawable/GuiDraw.java b/src/main/java/com/cleanroommc/modularui/drawable/GuiDraw.java index 1f455168f..3c34b2e12 100644 --- a/src/main/java/com/cleanroommc/modularui/drawable/GuiDraw.java +++ b/src/main/java/com/cleanroommc/modularui/drawable/GuiDraw.java @@ -338,7 +338,9 @@ public static void drawFluidTexture(FluidStack content, float x0, float y0, floa } public static void drawStandardSlotAmountText(int amount, String format, Area area) { - drawAmountText(amount, format, 1, 1, area.width - 1, area.height - 1, Alignment.BottomRight); + if (amount != 1 || format != null) { + drawAmountText(amount, format, 0, 0, area.width, area.height, Alignment.BottomRight); + } } public static void drawScaledAmountText(int amount, String format, int x, int y, int width, int height, Alignment alignment, int border) { @@ -368,28 +370,33 @@ public static void drawScaledAmountText(int amount, String format, int x, int y, } public static void drawAmountText(int amount, String format, int x, int y, int width, int height, Alignment alignment) { - if (amount > 1 || format != null) { - String amountText = NumberFormat.AMOUNT_TEXT.format(amount); - if (format != null) { - amountText = format + amountText; - } - float scale = 1f; - if (amountText.length() == 3) { - scale = 0.8f; - } else if (amountText.length() == 4) { - scale = 0.6f; - } else if (amountText.length() > 4) { - scale = 0.5f; - } - textRenderer.setShadow(true); - textRenderer.setScale(scale); - textRenderer.setColor(Color.WHITE.main); - textRenderer.setAlignment(alignment, width, height); - textRenderer.setPos(x, y); - textRenderer.setHardWrapOnBorder(false); + String s = NumberFormat.AMOUNT_TEXT.format(amount); + if (format != null) s = format + s; + drawScaledAlignedTextInBox(s, x, y, width, height, alignment); + } + + public static void drawScaledAlignedTextInBox(String amountText, int x, int y, int width, int height, Alignment alignment) { + drawScaledAlignedTextInBox(amountText, x, y, width, height, alignment, 1f); + } + + public static void drawScaledAlignedTextInBox(String amountText, int x, int y, int width, int height, Alignment alignment, float maxScale) { + if (amountText == null || amountText.isEmpty()) return; + // setup text renderer + textRenderer.setShadow(true); + textRenderer.setScale(1f); + textRenderer.setColor(Color.WHITE.main); + textRenderer.setAlignment(alignment, width, height); + textRenderer.setPos(x, y); + textRenderer.setHardWrapOnBorder(false); + if (amountText.length() > 2 && width > 16) { // we know that numbers below 100 will always fit in standard slots + // simulate and calculate scale with width + textRenderer.setSimulate(true); textRenderer.draw(amountText); - textRenderer.setHardWrapOnBorder(true); + textRenderer.setSimulate(false); + textRenderer.setScale(Math.min(maxScale, width / textRenderer.getLastActualWidth())); } + textRenderer.draw(amountText); + textRenderer.setHardWrapOnBorder(true); } /*public static void drawSprite(TextureAtlasSprite sprite, float x0, float y0, float w, float h) { diff --git a/src/main/java/com/cleanroommc/modularui/test/TestTile.java b/src/main/java/com/cleanroommc/modularui/test/TestTile.java index 20d283f77..0d24d5a6c 100644 --- a/src/main/java/com/cleanroommc/modularui/test/TestTile.java +++ b/src/main/java/com/cleanroommc/modularui/test/TestTile.java @@ -45,7 +45,6 @@ import com.cleanroommc.modularui.widgets.DynamicSyncedWidget; import com.cleanroommc.modularui.widgets.EntityDisplayWidget; import com.cleanroommc.modularui.widgets.Expandable; -import com.cleanroommc.modularui.widgets.FluidDisplayWidget; import com.cleanroommc.modularui.widgets.ItemDisplayWidget; import com.cleanroommc.modularui.widgets.ListWidget; import com.cleanroommc.modularui.widgets.PageButton; @@ -106,7 +105,7 @@ public class TestTile extends TileEntity implements IGuiHolder { private static final Logger LOGGER = LogManager.getLogger("MUI2-Test"); private final FluidTank fluidTank = new FluidTank(10000); - private final FluidTank fluidTankPhantom = new FluidTank(Integer.MAX_VALUE); + private final FluidTank fluidTankPhantom = new FluidTank(500000); private long time = 0; private int val, val2 = 0; private String value = ""; @@ -342,6 +341,7 @@ public ModularPanel buildUI(PosGuiData guiData, PanelSyncManager syncManager, UI .child(new FluidSlot() .margin(2) .width(30) + .alwaysShowFull(false) .syncHandler(SyncHandlers.fluidSlot(this.fluidTankPhantom).phantom(true))) .child(new Column() .name("button and slots test 3") @@ -533,7 +533,7 @@ public ModularPanel openSecondWindow(PanelSyncManager syncManager, IPanelHandler panel.child(ButtonWidget.panelCloseButton()) .child(new ButtonWidget<>() .size(10).top(14).right(4) - .overlay((new FluidDrawable().setFluid(new FluidStack(FluidRegistry.WATER,100))),IKey.str("3")) + .overlay((new FluidDrawable().setFluid(new FluidStack(FluidRegistry.WATER, 100))), IKey.str("3")) .onMousePressed(mouseButton -> { panelSyncHandler.openPanel(); return true; diff --git a/src/main/java/com/cleanroommc/modularui/utils/MathUtils.java b/src/main/java/com/cleanroommc/modularui/utils/MathUtils.java index 2d327592a..5d7fcefd4 100644 --- a/src/main/java/com/cleanroommc/modularui/utils/MathUtils.java +++ b/src/main/java/com/cleanroommc/modularui/utils/MathUtils.java @@ -198,4 +198,14 @@ public static float arithmeticGeometricMean(float a, float b, int iterations) { if (--iterations == 0) return a; return arithmeticGeometricMean(a, b, iterations); } + + public static double rescaleLinear(double v, double fromMin, double fromMax, double toMin, double toMax) { + v = (v - fromMin) / (fromMax - fromMin); // reverse lerp + return toMin + (toMax - toMin) * v; // forward lerp + } + + public static float rescaleLinear(float v, float fromMin, float fromMax, float toMin, float toMax) { + v = (v - fromMin) / (fromMax - fromMin); // reverse lerp + return toMin + (toMax - toMin) * v; // forward lerp + } } diff --git a/src/main/java/com/cleanroommc/modularui/widget/sizer/Box.java b/src/main/java/com/cleanroommc/modularui/widget/sizer/Box.java index 2e09dcd38..631893d60 100644 --- a/src/main/java/com/cleanroommc/modularui/widget/sizer/Box.java +++ b/src/main/java/com/cleanroommc/modularui/widget/sizer/Box.java @@ -17,6 +17,7 @@ public class Box implements IAnimatable { public static final Box SHARED = new Box(); public static final Box ZERO = new Box(); + public static final Box ONE = new Box().all(1); protected int left; protected int top; @@ -179,4 +180,4 @@ public boolean equals(Object o) { public int hashCode() { return Objects.hash(this.left, this.top, this.right, this.bottom); } -} \ No newline at end of file +} diff --git a/src/main/java/com/cleanroommc/modularui/widgets/AbstractFluidDisplayWidget.java b/src/main/java/com/cleanroommc/modularui/widgets/AbstractFluidDisplayWidget.java new file mode 100644 index 000000000..36a70e89d --- /dev/null +++ b/src/main/java/com/cleanroommc/modularui/widgets/AbstractFluidDisplayWidget.java @@ -0,0 +1,149 @@ +package com.cleanroommc.modularui.widgets; + +import com.cleanroommc.modularui.api.ITheme; +import com.cleanroommc.modularui.drawable.GuiDraw; +import com.cleanroommc.modularui.screen.viewport.ModularGuiContext; +import com.cleanroommc.modularui.theme.WidgetThemeEntry; +import com.cleanroommc.modularui.utils.Alignment; +import com.cleanroommc.modularui.utils.MathUtils; +import com.cleanroommc.modularui.utils.NumberFormat; +import com.cleanroommc.modularui.utils.SIPrefix; +import com.cleanroommc.modularui.widget.Widget; +import com.cleanroommc.modularui.widget.sizer.Box; + +import net.minecraftforge.fluids.FluidStack; + +import org.jetbrains.annotations.Nullable; + +public abstract class AbstractFluidDisplayWidget> extends Widget { + + public static final String UNIT_BUCKET = "B"; + public static final String UNIT_LITER = "L"; + + private final Box contentPadding = new Box().all(1); + private String unit = UNIT_BUCKET; + private SIPrefix baseUnitPrefix = SIPrefix.Milli; + private boolean flipLighterThanAir = true; + + protected AbstractFluidDisplayWidget() { + size(18); + } + + @Override + protected WidgetThemeEntry getWidgetThemeInternal(ITheme theme) { + return theme.getFluidSlotTheme(); + } + + @Override + public void draw(ModularGuiContext context, WidgetThemeEntry widgetTheme) { + FluidStack fluid = getFluidStack(); + if (fluid == null) return; + int x = this.contentPadding.getLeft(); + int y = this.contentPadding.getTop(); + int w = getArea().width - this.contentPadding.horizontal(); + int h = getArea().height - this.contentPadding.vertical(); + float c = getCapacity(); + if (c > 0 && fluid.amount > 0) { + int newH = (int) MathUtils.rescaleLinear(fluid.amount, 0, c, 1, h); + if (!this.flipLighterThanAir || !fluid.getFluid().isLighterThanAir()) y += h - newH; + h = newH; + } + GuiDraw.drawFluidTexture(fluid, x, y, w, h, context.getCurrentDrawingZ()); + } + + @Override + public void drawOverlay(ModularGuiContext context, WidgetThemeEntry widgetTheme) { + super.drawOverlay(context, widgetTheme); + FluidStack fluid = getFluidStack(); + if (fluid != null && displayAmountText()) { + String s = NumberFormat.format(getBaseUnitAmount(fluid.amount), NumberFormat.AMOUNT_TEXT) + getBaseUnit(); + // mc doesn't consider the 1px border in item slots for amount text, but it looks weird when it touches the left border, so + // we only apply padding there + GuiDraw.drawScaledAlignedTextInBox(s, this.contentPadding.getLeft(), 0, getArea().width - this.contentPadding.getLeft(), getArea().height, Alignment.BottomRight); + } + } + + protected abstract boolean displayAmountText(); + + @Nullable + protected abstract FluidStack getFluidStack(); + + /** + * Return a positive value if the fluid should be drawn partly depending on the amount filled. + * + * @return capacity in milli buckets or zero/negative if fluid should always be drawn full + */ + protected int getCapacity() { + return 0; + } + + public double getBaseUnitAmount(double amount) { + return amount * getBaseUnitSiPrefix().factor; + } + + public final String getUnit() { + return getBaseUnitSiPrefix().stringSymbol + getBaseUnit(); + } + + public String getBaseUnit() { + return this.unit; + } + + public SIPrefix getBaseUnitSiPrefix() { + return this.baseUnitPrefix; + } + + public boolean isFlipLighterThanAir() { + return flipLighterThanAir; + } + + public W contentPadding(int left, int right, int top, int bottom) { + this.contentPadding.all(left, right, top, bottom); + return getThis(); + } + + public W contentPadding(int horizontal, int vertical) { + this.contentPadding.all(horizontal, vertical); + return getThis(); + } + + public W contentPadding(int all) { + this.contentPadding.all(all); + return getThis(); + } + + public W contentPaddingLeft(int val) { + this.contentPadding.left(val); + return getThis(); + } + + public W contentPaddingRight(int val) { + this.contentPadding.right(val); + return getThis(); + } + + public W contentPaddingTop(int val) { + this.contentPadding.top(val); + return getThis(); + } + + public W contentPaddingBottom(int val) { + this.contentPadding.bottom(val); + return getThis(); + } + + public W fluidUnit(String baseUnitSymbol, SIPrefix baseUnitPrefix) { + this.unit = baseUnitSymbol; + this.baseUnitPrefix = baseUnitPrefix; + return getThis(); + } + + /** + * Determines if a partially filled fluid should be drawn from the top instead of the bottom if the current fluid is lighter than air. + * When the full fluid is drawn (when {@link #getCapacity()} returns 0) this does nothing. + */ + public W flipLighterThanAir(boolean flipLighterThanAir) { + this.flipLighterThanAir = flipLighterThanAir; + return getThis(); + } +} diff --git a/src/main/java/com/cleanroommc/modularui/widgets/FluidDisplayWidget.java b/src/main/java/com/cleanroommc/modularui/widgets/FluidDisplayWidget.java index 4ae80a93f..7ae6db1d4 100644 --- a/src/main/java/com/cleanroommc/modularui/widgets/FluidDisplayWidget.java +++ b/src/main/java/com/cleanroommc/modularui/widgets/FluidDisplayWidget.java @@ -1,63 +1,40 @@ package com.cleanroommc.modularui.widgets; -import com.cleanroommc.modularui.drawable.text.TextRenderer; -import com.cleanroommc.modularui.utils.Alignment; -import com.cleanroommc.modularui.utils.Color; -import com.cleanroommc.modularui.utils.NumberFormat; -import com.cleanroommc.modularui.widget.sizer.Area; +import com.cleanroommc.modularui.api.value.ISyncOrValue; +import com.cleanroommc.modularui.api.value.IValue; +import com.cleanroommc.modularui.screen.RichTooltip; -import net.minecraft.client.Minecraft; -import net.minecraft.client.gui.FontRenderer; import net.minecraftforge.fluids.FluidStack; -import com.cleanroommc.modularui.api.ITheme; -import com.cleanroommc.modularui.api.value.IValue; -import com.cleanroommc.modularui.drawable.GuiDraw; -import com.cleanroommc.modularui.screen.viewport.ModularGuiContext; -import com.cleanroommc.modularui.theme.WidgetThemeEntry; -import com.cleanroommc.modularui.value.ObjectValue; -import com.cleanroommc.modularui.value.sync.GenericSyncValue; -import com.cleanroommc.modularui.value.sync.SyncHandler; -import com.cleanroommc.modularui.widget.Widget; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; -public class FluidDisplayWidget extends Widget { +import java.util.function.BiConsumer; + +public class FluidDisplayWidget extends AbstractFluidDisplayWidget { private IValue value; - private boolean displayAmount = false; + private boolean displayAmount = true; @Override - public boolean isValidSyncHandler(SyncHandler syncHandler) { - if (syncHandler instanceof GenericSyncValuegenericSyncValue && genericSyncValue.isOfType(FluidStack.class)) { - this.value = genericSyncValue.cast(); - return true; - } - return false; + public boolean isValidSyncOrValue(@NotNull ISyncOrValue syncOrValue) { + return syncOrValue.isValueOfType(FluidStack.class); } @Override - protected WidgetThemeEntry getWidgetThemeInternal(ITheme theme) { - return theme.getItemSlotTheme(); + protected void setSyncOrValue(@NotNull ISyncOrValue syncOrValue) { + super.setSyncOrValue(syncOrValue); + this.value = syncOrValue.castValueNullable(FluidStack.class); } @Override - public void draw(ModularGuiContext context, WidgetThemeEntry widgetTheme) { - FluidStack fluid = value.getValue(); - if (fluid == null) return; - GuiDraw.drawFluidTexture(fluid, 0, 0, getArea().width, getArea().height, context.getCurrentDrawingZ()); - if (this.displayAmount) { - GuiDraw.drawScaledAmountText(fluid.amount, null, 1, 1, this.getArea().width-1, - this.getArea().height-1, Alignment.BottomRight, 1); - } - } - - public FluidDisplayWidget fluid(IValue fluidSupplier) { - this.value = fluidSupplier; - setValue(fluidSupplier); - return this; + protected boolean displayAmountText() { + return this.displayAmount; } - public FluidDisplayWidget fluid(FluidStack fluidStack) { - return fluid(new ObjectValue<>(fluidStack)); + @Override + protected @Nullable FluidStack getFluidStack() { + return this.value != null ? this.value.getValue() : null; } public FluidDisplayWidget displayAmount(boolean displayAmount) { @@ -65,4 +42,7 @@ public FluidDisplayWidget displayAmount(boolean displayAmount) { return this; } + public FluidDisplayWidget fluidTooltip(BiConsumer tooltip) { + return tooltipAutoUpdate(true).tooltipBuilder(t -> tooltip.accept(t, getFluidStack())); + } } diff --git a/src/main/java/com/cleanroommc/modularui/widgets/slot/FluidSlot.java b/src/main/java/com/cleanroommc/modularui/widgets/slot/FluidSlot.java index e37509ed1..1fbe5575a 100644 --- a/src/main/java/com/cleanroommc/modularui/widgets/slot/FluidSlot.java +++ b/src/main/java/com/cleanroommc/modularui/widgets/slot/FluidSlot.java @@ -8,7 +8,6 @@ import com.cleanroommc.modularui.api.value.ISyncOrValue; import com.cleanroommc.modularui.api.widget.Interactable; import com.cleanroommc.modularui.drawable.GuiDraw; -import com.cleanroommc.modularui.drawable.text.TextRenderer; import com.cleanroommc.modularui.integration.recipeviewer.RecipeViewerGhostIngredientSlot; import com.cleanroommc.modularui.integration.recipeviewer.RecipeViewerIngredientProvider; import com.cleanroommc.modularui.network.NetworkUtils; @@ -16,15 +15,10 @@ import com.cleanroommc.modularui.screen.viewport.ModularGuiContext; import com.cleanroommc.modularui.theme.SlotTheme; import com.cleanroommc.modularui.theme.WidgetThemeEntry; -import com.cleanroommc.modularui.utils.Alignment; -import com.cleanroommc.modularui.utils.Color; -import com.cleanroommc.modularui.utils.GlStateManager; import com.cleanroommc.modularui.utils.MouseData; -import com.cleanroommc.modularui.utils.NumberFormat; import com.cleanroommc.modularui.utils.Platform; -import com.cleanroommc.modularui.utils.SIPrefix; import com.cleanroommc.modularui.value.sync.FluidSlotSyncHandler; -import com.cleanroommc.modularui.widget.Widget; +import com.cleanroommc.modularui.widgets.AbstractFluidDisplayWidget; import net.minecraft.item.ItemStack; import net.minecraftforge.fluids.FluidContainerRegistry; @@ -33,17 +27,15 @@ import net.minecraftforge.fluids.IFluidTank; import gregtech.api.util.GTUtility; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.lwjgl.input.Keyboard; import java.text.DecimalFormat; -public class FluidSlot extends Widget implements Interactable, RecipeViewerGhostIngredientSlot, RecipeViewerIngredientProvider { +public class FluidSlot extends AbstractFluidDisplayWidget implements Interactable, RecipeViewerGhostIngredientSlot, RecipeViewerIngredientProvider { - public static final int DEFAULT_SIZE = 18; - public static final String UNIT_BUCKET = "B"; - public static final String UNIT_LITER = "L"; private static final DecimalFormat TOOLTIP_FORMAT = new DecimalFormat("#.##"); private static final IFluidTank EMPTY = new FluidTank(0); @@ -52,16 +44,11 @@ public class FluidSlot extends Widget implements Interactable, Recipe TOOLTIP_FORMAT.setGroupingSize(3); } - private final TextRenderer textRenderer = new TextRenderer(); private FluidSlotSyncHandler syncHandler; - private int contentOffsetX = 1, contentOffsetY = 1; private boolean alwaysShowFull = true; - @Nullable - private IDrawable overlayTexture = null; public FluidSlot() { - size(DEFAULT_SIZE); - tooltip().setAutoUpdate(true);//.setHasTitleMargin(true); + tooltip().setAutoUpdate(true); tooltipBuilder(this::addToolTip); } @@ -120,29 +107,6 @@ public String formatFluidTooltipAmount(double amount) { return TOOLTIP_FORMAT.format(amount); } - protected double getBaseUnitAmount(double amount) { - return amount * getBaseUnitSiPrefix().factor; - } - - protected final String getUnit() { - return getBaseUnitSiPrefix().stringSymbol + getBaseUnit(); - } - - protected String getBaseUnit() { - return UNIT_LITER; - } - - protected SIPrefix getBaseUnitSiPrefix() { - return SIPrefix.One; - } - - @Override - public void onInit() { - this.textRenderer.setShadow(true); - this.textRenderer.setScale(0.5f); - this.textRenderer.setColor(Color.WHITE.main); - } - @Override public boolean isValidSyncOrValue(@NotNull ISyncOrValue syncOrValue) { return syncOrValue.isTypeOrEmpty(FluidSlotSyncHandler.class); @@ -155,28 +119,8 @@ protected void setSyncOrValue(@NotNull ISyncOrValue syncOrValue) { } @Override - public void draw(ModularGuiContext context, WidgetThemeEntry widgetTheme) { - IFluidTank fluidTank = getFluidTank(); - FluidStack content = getFluidStack(); - if (content != null) { - float y = this.contentOffsetY; - float height = getArea().height - y * 2; - if (!this.alwaysShowFull) { - float newHeight = height * content.amount * 1f / fluidTank.getCapacity(); - y += height - newHeight; - height = newHeight; - } - GuiDraw.drawFluidTexture(content, this.contentOffsetX, y, getArea().width - this.contentOffsetX * 2, height, 0); - } - if (this.overlayTexture != null) { - this.overlayTexture.drawAtZeroPadded(context, getArea(), getActiveWidgetTheme(widgetTheme, isHovering())); - } - if (content != null && this.syncHandler.controlsAmount()) { - String s = NumberFormat.format(getBaseUnitAmount(content.amount), NumberFormat.AMOUNT_TEXT) + getBaseUnit(); - this.textRenderer.setAlignment(Alignment.CenterRight, getArea().width - this.contentOffsetX - 1f); - this.textRenderer.setPos((int) (this.contentOffsetX + 0.5f), (int) (getArea().height - 5.5f)); - this.textRenderer.draw(s); - } + protected boolean displayAmountText() { + return this.syncHandler == null || this.syncHandler.controlsAmount(); } @Override @@ -241,6 +185,11 @@ public boolean onKeyRelease(char typedChar, int keyCode) { return Interactable.super.onKeyRelease(typedChar, keyCode); } + @Override + protected int getCapacity() { + return this.alwaysShowFull ? 0 : getFluidTank().getCapacity(); + } + @Nullable public FluidStack getFluidStack() { return this.syncHandler == null ? null : this.syncHandler.getValue(); @@ -257,10 +206,10 @@ public IFluidTank getFluidTank() { * @param x x offset * @param y y offset */ + @ApiStatus.ScheduledForRemoval(inVersion = "3.2.0") + @Deprecated public FluidSlot contentOffset(int x, int y) { - this.contentOffsetX = x; - this.contentOffsetY = y; - return this; + return contentPaddingLeft(x).contentPaddingTop(y); } /** @@ -274,9 +223,10 @@ public FluidSlot alwaysShowFull(boolean alwaysShowFull) { /** * @param overlayTexture texture that is rendered on top of the fluid */ + @ApiStatus.ScheduledForRemoval(inVersion = "3.2.0") + @Deprecated public FluidSlot overlayTexture(@Nullable IDrawable overlayTexture) { - this.overlayTexture = overlayTexture; - return this; + return overlay(overlayTexture); } public FluidSlot syncHandler(IFluidTank fluidTank) { From 8e2e6cb8743fc7c0e3b1a2b9dad9e81e9b02cda0 Mon Sep 17 00:00:00 2001 From: brachy84 Date: Sun, 14 Dec 2025 17:18:08 +0100 Subject: [PATCH 34/50] fix --- .../cleanroommc/modularui/CommonProxy.java | 14 --------- .../modularui/core/mixinplugin/Mixins.java | 5 ++-- .../early/minecraft/EntityPlayerMPMixin.java | 29 +++++++++++++++++++ .../cleanroommc/modularui/drawable/Icon.java | 3 +- .../modularui/drawable/graph/GraphAxis.java | 3 +- .../modularui/factory/GuiManager.java | 1 + .../modularui/screen/UISettings.java | 2 ++ .../cleanroommc/modularui/test/TestGuis.java | 4 +-- .../modularui/value/sync/ISyncRegistrar.java | 4 +-- .../widgets/AbstractFluidDisplayWidget.java | 7 +++-- .../modularui/widgets/ItemDisplayWidget.java | 2 +- .../modularui/widgets/slot/FluidSlot.java | 1 + .../modularui/widgets/slot/ModularSlot.java | 7 ++++- 13 files changed, 53 insertions(+), 29 deletions(-) create mode 100644 src/main/java/com/cleanroommc/modularui/core/mixins/early/minecraft/EntityPlayerMPMixin.java diff --git a/src/main/java/com/cleanroommc/modularui/CommonProxy.java b/src/main/java/com/cleanroommc/modularui/CommonProxy.java index 6541d8656..a99d91c2e 100644 --- a/src/main/java/com/cleanroommc/modularui/CommonProxy.java +++ b/src/main/java/com/cleanroommc/modularui/CommonProxy.java @@ -62,20 +62,6 @@ public Timer getTimer60Fps() { throw new UnsupportedOperationException(); } - @SubscribeEvent - public void onOpenContainer(PlayerContainerEvent.Open event) { - if (event.getContainer() instanceof ModularContainer container) { - container.onModularContainerOpened(); - } - } - - @SubscribeEvent - public void onCloseContainer(PlayerContainerEvent.Close event) { - if (event.getContainer() instanceof ModularContainer container) { - container.onModularContainerClosed(); - } - } - @SubscribeEvent public void onTick(TickEvent.PlayerTickEvent event) { if (event.player.openContainer instanceof ModularContainer container) { diff --git a/src/main/java/com/cleanroommc/modularui/core/mixinplugin/Mixins.java b/src/main/java/com/cleanroommc/modularui/core/mixinplugin/Mixins.java index 3323fa546..b223f0fcd 100644 --- a/src/main/java/com/cleanroommc/modularui/core/mixinplugin/Mixins.java +++ b/src/main/java/com/cleanroommc/modularui/core/mixinplugin/Mixins.java @@ -8,8 +8,8 @@ public enum Mixins implements IMixins { MINECRAFT(new MixinBuilder() .addClientMixins( - "minecraft.FontRendererAccessor", "forge.ForgeHooksClientMixin", + "minecraft.FontRendererAccessor", "minecraft.GuiAccessor", "minecraft.GuiButtonMixin", "minecraft.GuiContainerAccessor", @@ -19,8 +19,9 @@ public enum Mixins implements IMixins { "minecraft.MinecraftMixin", "minecraft.SimpleResourceAccessor") .addCommonMixins( - "minecraft.EntityAccessor", "minecraft.ContainerAccessor", + "minecraft.EntityAccessor", + "minecraft.EntityPlayerMPMixin", "minecraft.InventoryCraftingAccessor", "forge.SimpleNetworkWrapperMixin") .setPhase(Phase.EARLY)), diff --git a/src/main/java/com/cleanroommc/modularui/core/mixins/early/minecraft/EntityPlayerMPMixin.java b/src/main/java/com/cleanroommc/modularui/core/mixins/early/minecraft/EntityPlayerMPMixin.java new file mode 100644 index 000000000..f0145a345 --- /dev/null +++ b/src/main/java/com/cleanroommc/modularui/core/mixins/early/minecraft/EntityPlayerMPMixin.java @@ -0,0 +1,29 @@ +package com.cleanroommc.modularui.core.mixins.early.minecraft; + +import com.cleanroommc.modularui.screen.ModularContainer; + +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.entity.player.EntityPlayerMP; +import net.minecraft.world.World; + +import com.mojang.authlib.GameProfile; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(EntityPlayerMP.class) +public abstract class EntityPlayerMPMixin extends EntityPlayer { + + public EntityPlayerMPMixin(World p_i45324_1_, GameProfile p_i45324_2_) { + super(p_i45324_1_, p_i45324_2_); + } + + @Inject(method = "closeContainer", at = @At(value = "INVOKE", target = "Lnet/minecraft/inventory/Container;onContainerClosed(Lnet/minecraft/entity/player/EntityPlayer;)V")) + public void closeContainer(CallbackInfo ci) { + // replicates the container closed event listener from 1.12 + if (this.openContainer instanceof ModularContainer mc) { + mc.onModularContainerClosed(); + } + } +} diff --git a/src/main/java/com/cleanroommc/modularui/drawable/Icon.java b/src/main/java/com/cleanroommc/modularui/drawable/Icon.java index b3338074d..17a7f0efc 100644 --- a/src/main/java/com/cleanroommc/modularui/drawable/Icon.java +++ b/src/main/java/com/cleanroommc/modularui/drawable/Icon.java @@ -10,7 +10,6 @@ import com.cleanroommc.modularui.utils.JsonHelper; import com.cleanroommc.modularui.widget.sizer.Box; -import net.minecraftforge.fml.relauncher.FMLLaunchHandler; import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; @@ -81,7 +80,7 @@ public void draw(GuiContext context, int x, int y, int width, int height, Widget } else if (this.height <= 0) { // width is set, so adjust height to width height = (int) (width / this.aspectRatio); - } else if (FMLLaunchHandler.isDeobfuscatedEnvironment()) { + } else if (ModularUI.isDevEnv) { ModularUI.LOGGER.error("Aspect ration in Icon can't be applied when width and height are specified"); // remove aspect ratio to avoid log spamming, it does nothing in the current state anyway this.aspectRatio = 0; diff --git a/src/main/java/com/cleanroommc/modularui/drawable/graph/GraphAxis.java b/src/main/java/com/cleanroommc/modularui/drawable/graph/GraphAxis.java index a1433b3ca..5ecba7164 100644 --- a/src/main/java/com/cleanroommc/modularui/drawable/graph/GraphAxis.java +++ b/src/main/java/com/cleanroommc/modularui/drawable/graph/GraphAxis.java @@ -1,13 +1,12 @@ package com.cleanroommc.modularui.drawable.graph; import com.cleanroommc.modularui.api.GuiAxis; +import com.cleanroommc.modularui.drawable.BufferBuilder; import com.cleanroommc.modularui.drawable.GuiDraw; import com.cleanroommc.modularui.drawable.text.TextRenderer; import com.cleanroommc.modularui.utils.Alignment; import com.cleanroommc.modularui.utils.DAM; -import net.minecraft.client.renderer.BufferBuilder; - import org.jetbrains.annotations.ApiStatus; import java.text.DecimalFormat; diff --git a/src/main/java/com/cleanroommc/modularui/factory/GuiManager.java b/src/main/java/com/cleanroommc/modularui/factory/GuiManager.java index a86ecbc95..59204ed81 100644 --- a/src/main/java/com/cleanroommc/modularui/factory/GuiManager.java +++ b/src/main/java/com/cleanroommc/modularui/factory/GuiManager.java @@ -90,6 +90,7 @@ public static void open(@NotNull UIFactory factory, @NotN player.openContainer = container; player.openContainer.windowId = windowId; player.openContainer.addCraftingToCrafters(player); + container.onModularContainerOpened(); } @ApiStatus.Internal diff --git a/src/main/java/com/cleanroommc/modularui/screen/UISettings.java b/src/main/java/com/cleanroommc/modularui/screen/UISettings.java index 74ec9e43f..b5a379874 100644 --- a/src/main/java/com/cleanroommc/modularui/screen/UISettings.java +++ b/src/main/java/com/cleanroommc/modularui/screen/UISettings.java @@ -9,6 +9,8 @@ import net.minecraft.client.gui.inventory.GuiContainer; import net.minecraft.entity.player.EntityPlayer; +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.Nullable; diff --git a/src/main/java/com/cleanroommc/modularui/test/TestGuis.java b/src/main/java/com/cleanroommc/modularui/test/TestGuis.java index 6ba9408f2..5564ba6af 100644 --- a/src/main/java/com/cleanroommc/modularui/test/TestGuis.java +++ b/src/main/java/com/cleanroommc/modularui/test/TestGuis.java @@ -26,9 +26,9 @@ import com.cleanroommc.modularui.theme.WidgetTheme; import com.cleanroommc.modularui.utils.Alignment; import com.cleanroommc.modularui.utils.Color; -import com.cleanroommc.modularui.utils.GlStateManager; import com.cleanroommc.modularui.utils.ColorShade; import com.cleanroommc.modularui.utils.DAM; +import com.cleanroommc.modularui.utils.GlStateManager; import com.cleanroommc.modularui.utils.Interpolation; import com.cleanroommc.modularui.utils.Interpolations; import com.cleanroommc.modularui.utils.Platform; @@ -224,7 +224,7 @@ public void onInit() { .transform((widget, stack) -> stack.translate(post.getValue(), 0))) .child(IKey.str("the ").asWidget() .transform((widget, stack) -> stack.translate(0, the.getValue()))) - .child(IKey.str("fucking ").style(TextFormatting.OBFUSCATED).asWidget() + .child(IKey.str("fucking ").style(IKey.OBFUSCATED).asWidget() .transform((widget, stack) -> stack.translate(extraordinary.getValue(), 0)))) .child(IKey.str("LOOOOGG!!!!").asWidget() .paddingTop(4) diff --git a/src/main/java/com/cleanroommc/modularui/value/sync/ISyncRegistrar.java b/src/main/java/com/cleanroommc/modularui/value/sync/ISyncRegistrar.java index 422714d9a..38717ea7a 100644 --- a/src/main/java/com/cleanroommc/modularui/value/sync/ISyncRegistrar.java +++ b/src/main/java/com/cleanroommc/modularui/value/sync/ISyncRegistrar.java @@ -2,12 +2,12 @@ import com.cleanroommc.modularui.api.IPanelHandler; import com.cleanroommc.modularui.api.ISyncedAction; +import com.cleanroommc.modularui.utils.item.PlayerMainInvWrapper; import com.cleanroommc.modularui.widgets.slot.ModularSlot; import com.cleanroommc.modularui.widgets.slot.SlotGroup; import net.minecraft.entity.player.EntityPlayer; -import net.minecraftforge.fml.relauncher.Side; -import net.minecraftforge.items.wrapper.PlayerMainInvWrapper; +import cpw.mods.fml.relauncher.Side; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; diff --git a/src/main/java/com/cleanroommc/modularui/widgets/AbstractFluidDisplayWidget.java b/src/main/java/com/cleanroommc/modularui/widgets/AbstractFluidDisplayWidget.java index 36a70e89d..4d97af073 100644 --- a/src/main/java/com/cleanroommc/modularui/widgets/AbstractFluidDisplayWidget.java +++ b/src/main/java/com/cleanroommc/modularui/widgets/AbstractFluidDisplayWidget.java @@ -21,8 +21,8 @@ public abstract class AbstractFluidDisplayWidget widgetTheme) { float c = getCapacity(); if (c > 0 && fluid.amount > 0) { int newH = (int) MathUtils.rescaleLinear(fluid.amount, 0, c, 1, h); - if (!this.flipLighterThanAir || !fluid.getFluid().isLighterThanAir()) y += h - newH; + // 1.12 has a method called isLighterThanAir(), using isGaseous() as a replacement here + if (!this.flipLighterThanAir || !fluid.getFluid().isGaseous()) y += h - newH; h = newH; } GuiDraw.drawFluidTexture(fluid, x, y, w, h, context.getCurrentDrawingZ()); diff --git a/src/main/java/com/cleanroommc/modularui/widgets/ItemDisplayWidget.java b/src/main/java/com/cleanroommc/modularui/widgets/ItemDisplayWidget.java index 3f63382c2..37f55f80a 100644 --- a/src/main/java/com/cleanroommc/modularui/widgets/ItemDisplayWidget.java +++ b/src/main/java/com/cleanroommc/modularui/widgets/ItemDisplayWidget.java @@ -83,7 +83,7 @@ public ItemDisplayWidget displayAmount(boolean displayAmount) { } @Override - public @Nullable Object getIngredient() { + public @Nullable ItemStack getStackForRecipeViewer() { return value.getValue(); } } diff --git a/src/main/java/com/cleanroommc/modularui/widgets/slot/FluidSlot.java b/src/main/java/com/cleanroommc/modularui/widgets/slot/FluidSlot.java index 1fbe5575a..87d63c7ec 100644 --- a/src/main/java/com/cleanroommc/modularui/widgets/slot/FluidSlot.java +++ b/src/main/java/com/cleanroommc/modularui/widgets/slot/FluidSlot.java @@ -15,6 +15,7 @@ import com.cleanroommc.modularui.screen.viewport.ModularGuiContext; import com.cleanroommc.modularui.theme.SlotTheme; import com.cleanroommc.modularui.theme.WidgetThemeEntry; +import com.cleanroommc.modularui.utils.GlStateManager; import com.cleanroommc.modularui.utils.MouseData; import com.cleanroommc.modularui.utils.Platform; import com.cleanroommc.modularui.value.sync.FluidSlotSyncHandler; diff --git a/src/main/java/com/cleanroommc/modularui/widgets/slot/ModularSlot.java b/src/main/java/com/cleanroommc/modularui/widgets/slot/ModularSlot.java index 2099f39af..60e509e18 100644 --- a/src/main/java/com/cleanroommc/modularui/widgets/slot/ModularSlot.java +++ b/src/main/java/com/cleanroommc/modularui/widgets/slot/ModularSlot.java @@ -1,11 +1,15 @@ package com.cleanroommc.modularui.widgets.slot; import com.cleanroommc.modularui.utils.item.IItemHandler; +import com.cleanroommc.modularui.utils.item.PlayerInvWrapper; +import com.cleanroommc.modularui.utils.item.PlayerMainInvWrapper; import com.cleanroommc.modularui.utils.item.SlotItemHandler; import com.cleanroommc.modularui.value.sync.ItemSlotSH; import net.minecraft.client.renderer.texture.TextureAtlasSprite; import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.entity.player.InventoryPlayer; +import net.minecraft.inventory.Slot; import net.minecraft.item.ItemStack; import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; @@ -240,6 +244,7 @@ public static boolean isPlayerSlot(Slot slot) { } public static boolean isPlayerSlot(SlotItemHandler slot) { - return slot.getItemHandler() instanceof PlayerInvWrapper || slot.getItemHandler() instanceof PlayerMainInvWrapper; + return slot.getItemHandler() instanceof PlayerInvWrapper || slot.getItemHandler() instanceof PlayerMainInvWrapper || + slot.getItemHandler() instanceof com.gtnewhorizons.modularui.api.forge.PlayerMainInvWrapper; } } From 7e8ce68f43fd3048ef456323a539f589e27b62af Mon Sep 17 00:00:00 2001 From: brachy84 Date: Sun, 14 Dec 2025 17:19:58 +0100 Subject: [PATCH 35/50] recipe viewer ingredient provider for fluid display widget (cherry picked from commit 657519fc262ccdfad8c66b21eb22da7b4aa8f31c) --- .../widgets/AbstractFluidDisplayWidget.java | 14 +++++++++++++- .../modularui/widgets/slot/FluidSlot.java | 11 +---------- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/src/main/java/com/cleanroommc/modularui/widgets/AbstractFluidDisplayWidget.java b/src/main/java/com/cleanroommc/modularui/widgets/AbstractFluidDisplayWidget.java index 4d97af073..47234fad3 100644 --- a/src/main/java/com/cleanroommc/modularui/widgets/AbstractFluidDisplayWidget.java +++ b/src/main/java/com/cleanroommc/modularui/widgets/AbstractFluidDisplayWidget.java @@ -1,7 +1,9 @@ package com.cleanroommc.modularui.widgets; +import com.cleanroommc.modularui.ModularUI; import com.cleanroommc.modularui.api.ITheme; import com.cleanroommc.modularui.drawable.GuiDraw; +import com.cleanroommc.modularui.integration.recipeviewer.RecipeViewerIngredientProvider; import com.cleanroommc.modularui.screen.viewport.ModularGuiContext; import com.cleanroommc.modularui.theme.WidgetThemeEntry; import com.cleanroommc.modularui.utils.Alignment; @@ -11,11 +13,13 @@ import com.cleanroommc.modularui.widget.Widget; import com.cleanroommc.modularui.widget.sizer.Box; +import net.minecraft.item.ItemStack; import net.minecraftforge.fluids.FluidStack; +import gregtech.api.util.GTUtility; import org.jetbrains.annotations.Nullable; -public abstract class AbstractFluidDisplayWidget> extends Widget { +public abstract class AbstractFluidDisplayWidget> extends Widget implements RecipeViewerIngredientProvider { public static final String UNIT_BUCKET = "B"; public static final String UNIT_LITER = "L"; @@ -69,6 +73,14 @@ public void drawOverlay(ModularGuiContext context, WidgetThemeEntry widgetThe @Nullable protected abstract FluidStack getFluidStack(); + @Override + public @Nullable ItemStack getStackForRecipeViewer() { + if (ModularUI.Mods.GT5U.isLoaded()) { + return GTUtility.getFluidDisplayStack(getFluidStack(), false); + } + return null; + } + /** * Return a positive value if the fluid should be drawn partly depending on the amount filled. * diff --git a/src/main/java/com/cleanroommc/modularui/widgets/slot/FluidSlot.java b/src/main/java/com/cleanroommc/modularui/widgets/slot/FluidSlot.java index 87d63c7ec..1023cdfc8 100644 --- a/src/main/java/com/cleanroommc/modularui/widgets/slot/FluidSlot.java +++ b/src/main/java/com/cleanroommc/modularui/widgets/slot/FluidSlot.java @@ -9,7 +9,6 @@ import com.cleanroommc.modularui.api.widget.Interactable; import com.cleanroommc.modularui.drawable.GuiDraw; import com.cleanroommc.modularui.integration.recipeviewer.RecipeViewerGhostIngredientSlot; -import com.cleanroommc.modularui.integration.recipeviewer.RecipeViewerIngredientProvider; import com.cleanroommc.modularui.network.NetworkUtils; import com.cleanroommc.modularui.screen.RichTooltip; import com.cleanroommc.modularui.screen.viewport.ModularGuiContext; @@ -35,7 +34,7 @@ import java.text.DecimalFormat; -public class FluidSlot extends AbstractFluidDisplayWidget implements Interactable, RecipeViewerGhostIngredientSlot, RecipeViewerIngredientProvider { +public class FluidSlot extends AbstractFluidDisplayWidget implements Interactable, RecipeViewerGhostIngredientSlot { private static final DecimalFormat TOOLTIP_FORMAT = new DecimalFormat("#.##"); private static final IFluidTank EMPTY = new FluidTank(0); @@ -256,12 +255,4 @@ public boolean handleDragAndDrop(@NotNull ItemStack draggedStack, int button) { protected void setPhantomValue(@NotNull ItemStack draggedStack) { this.syncHandler.setValue(FluidContainerRegistry.getFluidForFilledItem(draggedStack)); } - - @Override - public @Nullable ItemStack getStackForRecipeViewer() { - if (ModularUI.Mods.GT5U.isLoaded()) { - return GTUtility.getFluidDisplayStack(getFluidStack(), false); - } - return null; - } } From 47779c3dcd5d1dbcfbbffde39342c4420071ebc3 Mon Sep 17 00:00:00 2001 From: brachy84 Date: Sun, 14 Dec 2025 20:03:38 +0100 Subject: [PATCH 36/50] remove useless alpha masking (cherry picked from commit b535bbd578f487b27b3fc656888d4e927d3bf160) --- .../java/com/cleanroommc/modularui/utils/Color.java | 1 - .../modularui/widget/InternalWidgetTree.java | 2 -- .../cleanroommc/modularui/widgets/slot/FluidSlot.java | 2 -- .../cleanroommc/modularui/widgets/slot/ItemSlot.java | 10 +++------- 4 files changed, 3 insertions(+), 12 deletions(-) diff --git a/src/main/java/com/cleanroommc/modularui/utils/Color.java b/src/main/java/com/cleanroommc/modularui/utils/Color.java index 3b947dc4c..a6c0c78f0 100644 --- a/src/main/java/com/cleanroommc/modularui/utils/Color.java +++ b/src/main/java/com/cleanroommc/modularui/utils/Color.java @@ -821,7 +821,6 @@ public static void setGlColorOpaque(int color) { */ @SideOnly(Side.CLIENT) public static void resetGlColor() { - GlStateManager.colorMask(true, true, true, true); setGlColorOpaque(WHITE.main); } diff --git a/src/main/java/com/cleanroommc/modularui/widget/InternalWidgetTree.java b/src/main/java/com/cleanroommc/modularui/widget/InternalWidgetTree.java index c61c88242..9c279305f 100644 --- a/src/main/java/com/cleanroommc/modularui/widget/InternalWidgetTree.java +++ b/src/main/java/com/cleanroommc/modularui/widget/InternalWidgetTree.java @@ -70,7 +70,6 @@ static void drawTree(IWidget parent, ModularGuiContext context, boolean ignoreEn GlStateManager.pushMatrix(); context.applyToOpenGl(); - GlStateManager.colorMask(true, true, true, true); if (canBeSeen) { // draw widget GlStateManager.color(1f, 1f, 1f, alpha); @@ -160,7 +159,6 @@ static void drawBackground(IWidget parent, ModularGuiContext context, boolean ig context.applyToOpenGl(); // draw widget - GlStateManager.colorMask(true, true, true, true); GlStateManager.color(1f, 1f, 1f, alpha); WidgetThemeEntry widgetTheme = parent.getWidgetTheme(context.getTheme()); parent.drawBackground(context, widgetTheme); diff --git a/src/main/java/com/cleanroommc/modularui/widgets/slot/FluidSlot.java b/src/main/java/com/cleanroommc/modularui/widgets/slot/FluidSlot.java index 1023cdfc8..447f949e9 100644 --- a/src/main/java/com/cleanroommc/modularui/widgets/slot/FluidSlot.java +++ b/src/main/java/com/cleanroommc/modularui/widgets/slot/FluidSlot.java @@ -127,9 +127,7 @@ protected boolean displayAmountText() { public void drawOverlay(ModularGuiContext context, WidgetThemeEntry widgetTheme) { super.drawOverlay(context, widgetTheme); if (isHovering()) { - GlStateManager.colorMask(true, true, true, false); GuiDraw.drawRect(1, 1, getArea().w() - 2, getArea().h() - 2, getSlotHoverColor()); - GlStateManager.colorMask(true, true, true, true); } } diff --git a/src/main/java/com/cleanroommc/modularui/widgets/slot/ItemSlot.java b/src/main/java/com/cleanroommc/modularui/widgets/slot/ItemSlot.java index 08063d11e..feeb1c46e 100644 --- a/src/main/java/com/cleanroommc/modularui/widgets/slot/ItemSlot.java +++ b/src/main/java/com/cleanroommc/modularui/widgets/slot/ItemSlot.java @@ -99,9 +99,7 @@ public void draw(ModularGuiContext context, WidgetThemeEntry widgetTheme) { protected void drawOverlay() { if (isHovering() && (!ModularUI.Mods.NEA.isLoaded() || NEAConfig.itemHoverOverlay)) { - GlStateManager.colorMask(true, true, true, false); GuiDraw.drawRect(1, 1, 16, 16, getSlotHoverColor()); - GlStateManager.colorMask(true, true, true, true); } } @@ -204,9 +202,7 @@ public ItemSlot tooltip(RichTooltip tooltip) { } public ItemSlot slot(ModularSlot slot) { - this.syncHandler = new ItemSlotSH(slot); - setSyncHandler(this.syncHandler); - return this; + return syncHandler(new ItemSlotSH(slot)); } public ItemSlot slot(IItemHandlerModifiable itemHandler, int index) { @@ -266,9 +262,9 @@ private void drawSlot(ModularSlot slotIn) { ((GuiAccessor) guiScreen).setZLevel(z); renderItem.zLevel = z; - if (!flag1) { + if (!doDrawItem) { if (isDragPreview) { - GuiDraw.drawRect(1, 1, 16, 16, -2130706433); + GuiDraw.drawRect(1, 1, 16, 16, 0x80FFFFFF); } itemstack = NEAAnimationHandler.injectVirtualStack(itemstack, guiContainer, slotIn); From 4461dcadcd88f1d8e555c19ed699f9d3b283fe1a Mon Sep 17 00:00:00 2001 From: brachy84 Date: Sat, 27 Dec 2025 16:36:45 +0100 Subject: [PATCH 37/50] dont just close container on gui stack pop (cherry picked from commit 96668d8714390126a51a9d400f55cbbc2961fad0) --- src/main/java/com/cleanroommc/modularui/api/MCHelper.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/cleanroommc/modularui/api/MCHelper.java b/src/main/java/com/cleanroommc/modularui/api/MCHelper.java index 232a1dbd1..07b8eec26 100644 --- a/src/main/java/com/cleanroommc/modularui/api/MCHelper.java +++ b/src/main/java/com/cleanroommc/modularui/api/MCHelper.java @@ -60,8 +60,8 @@ public static boolean closeScreen() { public static void popScreen(boolean openParentOnClose, GuiScreen parent) { EntityPlayer player = MCHelper.getPlayer(); if (player != null) { - // TODO: should we really close container here? - prepareCloseContainer(player); + // container should not just be closed here, however this means the gui stack only works with client only screens (except the root) + // TODO: figure out the necessity of a Container stack if (openParentOnClose) { Minecraft.getMinecraft().displayGuiScreen(parent); } else { From 2e0654b8787d08df008b7089120e3efd12948fdd Mon Sep 17 00:00:00 2001 From: brachy84 Date: Thu, 1 Jan 2026 10:20:14 +0100 Subject: [PATCH 38/50] remove some useless childIf overloads (cherry picked from commit 0a78acc7634bb960dbc02d63fe4c2ed686698698) --- .../modularui/api/widget/IParentWidget.java | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/src/main/java/com/cleanroommc/modularui/api/widget/IParentWidget.java b/src/main/java/com/cleanroommc/modularui/api/widget/IParentWidget.java index bcc87bca8..6f5cdc38f 100644 --- a/src/main/java/com/cleanroommc/modularui/api/widget/IParentWidget.java +++ b/src/main/java/com/cleanroommc/modularui/api/widget/IParentWidget.java @@ -1,6 +1,7 @@ package com.cleanroommc.modularui.api.widget; -import java.util.function.BooleanSupplier; +import org.jetbrains.annotations.ApiStatus; + import java.util.function.Supplier; public interface IParentWidget> { @@ -23,23 +24,18 @@ default W child(I child) { return getThis(); } + /** + * @deprecated use {@link #childIf(boolean, Supplier)} + */ + @ApiStatus.ScheduledForRemoval(inVersion = "3.2.0") + @Deprecated default W childIf(boolean condition, I child) { if (condition) return child(child); return getThis(); } - default W childIf(BooleanSupplier condition, I child) { - if (condition.getAsBoolean()) return child(child); - return getThis(); - } - default W childIf(boolean condition, Supplier child) { if (condition) return child(child.get()); return getThis(); } - - default W childIf(BooleanSupplier condition, Supplier child) { - if (condition.getAsBoolean()) return child(child.get()); - return getThis(); - } } From 31a6ecc092ed05e0324c45a99db54e7f518a650e Mon Sep 17 00:00:00 2001 From: brachy84 Date: Tue, 13 Jan 2026 14:44:46 +0100 Subject: [PATCH 39/50] remove empty file --- .../java/com/cleanroommc/modularui/keybind/KeyBindHandler.java | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 src/main/java/com/cleanroommc/modularui/keybind/KeyBindHandler.java diff --git a/src/main/java/com/cleanroommc/modularui/keybind/KeyBindHandler.java b/src/main/java/com/cleanroommc/modularui/keybind/KeyBindHandler.java deleted file mode 100644 index e69de29bb..000000000 From 1afc6b70cdb1bab46bd314237099cd51f9eca268 Mon Sep 17 00:00:00 2001 From: brachy84 Date: Thu, 1 Jan 2026 16:11:21 +0100 Subject: [PATCH 40/50] fix IDrawable.drawAtZero (cherry picked from commit 0ead83b2e8d0bbbc9babf38ba923140fa800f9da) --- .../java/com/cleanroommc/modularui/api/drawable/IDrawable.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/cleanroommc/modularui/api/drawable/IDrawable.java b/src/main/java/com/cleanroommc/modularui/api/drawable/IDrawable.java index 1d11d6e59..daea474cf 100644 --- a/src/main/java/com/cleanroommc/modularui/api/drawable/IDrawable.java +++ b/src/main/java/com/cleanroommc/modularui/api/drawable/IDrawable.java @@ -94,7 +94,7 @@ default void drawPadded(GuiContext context, Area area, WidgetTheme widgetTheme) */ @SideOnly(Side.CLIENT) default void drawAtZero(GuiContext context, Area area, WidgetTheme widgetTheme) { - draw(context, 0, 0, area.paddedWidth(), area.paddedHeight(), widgetTheme); + draw(context, 0, 0, area.width, area.height, widgetTheme); } /** From 37801b7506131aeba77e40c09d832dbd738a5a4f Mon Sep 17 00:00:00 2001 From: brachy84 Date: Fri, 2 Jan 2026 10:15:57 +0100 Subject: [PATCH 41/50] ability to override color on UITexture without theme (cherry picked from commit 666744fe1ebdeabc670055987e4fdef919e69e71) --- .../cleanroommc/modularui/ClientProxy.java | 5 +- .../drawable/AdaptableUITexture.java | 10 +++ .../modularui/drawable/DelegateDrawable.java | 64 +++++++++++++++++++ .../modularui/drawable/DelegateIcon.java | 1 - .../modularui/drawable/TiledUITexture.java | 10 +++ .../modularui/drawable/UITexture.java | 30 +++++++-- 6 files changed, 111 insertions(+), 9 deletions(-) create mode 100644 src/main/java/com/cleanroommc/modularui/drawable/DelegateDrawable.java diff --git a/src/main/java/com/cleanroommc/modularui/ClientProxy.java b/src/main/java/com/cleanroommc/modularui/ClientProxy.java index 1c831341f..0a92d6ce8 100644 --- a/src/main/java/com/cleanroommc/modularui/ClientProxy.java +++ b/src/main/java/com/cleanroommc/modularui/ClientProxy.java @@ -98,9 +98,8 @@ void preInit(FMLPreInitializationEvent event) { } catch (IOException | LWJGLException e) { throw new RuntimeException(e); } catch (Throwable e) { - ModularUI.LOGGER.info("Custom Cursors failed to load. This is likely because an incompatible LWJGL version was used like with CleanroomLoader."); - // TODO: proper lwjgl 3 support - // currently it seems this is not even triggered and the cursors are created correctly, but when the cursors are set nothing happens + ModularUI.LOGGER.info("Custom Cursors failed to load."); + // lwjgl3: currently it seems this is not even triggered and the cursors are created correctly, but when the cursors are set nothing happens } } diff --git a/src/main/java/com/cleanroommc/modularui/drawable/AdaptableUITexture.java b/src/main/java/com/cleanroommc/modularui/drawable/AdaptableUITexture.java index 274a09679..870926534 100644 --- a/src/main/java/com/cleanroommc/modularui/drawable/AdaptableUITexture.java +++ b/src/main/java/com/cleanroommc/modularui/drawable/AdaptableUITexture.java @@ -179,4 +179,14 @@ public boolean saveToJson(JsonObject json) { json.addProperty("tiled", this.tiled); return true; } + + @Override + protected AdaptableUITexture copy() { + return new AdaptableUITexture(location, u0, v0, u1, v1, colorType, nonOpaque, imageWidth, imageHeight, bl, bt, br, bb, tiled); + } + + @Override + public AdaptableUITexture withColorOverride(int color) { + return (AdaptableUITexture) super.withColorOverride(color); + } } diff --git a/src/main/java/com/cleanroommc/modularui/drawable/DelegateDrawable.java b/src/main/java/com/cleanroommc/modularui/drawable/DelegateDrawable.java new file mode 100644 index 000000000..3d2a5375d --- /dev/null +++ b/src/main/java/com/cleanroommc/modularui/drawable/DelegateDrawable.java @@ -0,0 +1,64 @@ +package com.cleanroommc.modularui.drawable; + +import com.cleanroommc.modularui.api.drawable.IDrawable; +import com.cleanroommc.modularui.screen.viewport.GuiContext; +import com.cleanroommc.modularui.theme.WidgetTheme; +import com.cleanroommc.modularui.widget.Widget; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public class DelegateDrawable implements IDrawable { + + @NotNull + private IDrawable drawable; + + public DelegateDrawable(@Nullable IDrawable drawable) { + setDrawable(drawable); + } + + // protected, so subclasses can define mutability + protected void setDrawable(@Nullable IDrawable drawable) { + this.drawable = drawable != null ? drawable : IDrawable.EMPTY; + } + + @NotNull + public IDrawable getWrappedDrawable() { + return drawable; + } + + @Override + public void draw(GuiContext context, int x, int y, int width, int height, WidgetTheme widgetTheme) { + this.drawable.draw(context, x, y, width, height, widgetTheme); + } + + @Override + public boolean canApplyTheme() { + return this.drawable.canApplyTheme(); + } + + @Override + public void applyColor(int themeColor) { + this.drawable.applyColor(themeColor); + } + + @Override + public int getDefaultWidth() { + return this.drawable.getDefaultWidth(); + } + + @Override + public int getDefaultHeight() { + return this.drawable.getDefaultHeight(); + } + + @Override + public Widget asWidget() { + return this.drawable.asWidget(); + } + + @Override + public Icon asIcon() { + return this.drawable.asIcon(); + } +} diff --git a/src/main/java/com/cleanroommc/modularui/drawable/DelegateIcon.java b/src/main/java/com/cleanroommc/modularui/drawable/DelegateIcon.java index 05c45ae4b..c9d07e073 100644 --- a/src/main/java/com/cleanroommc/modularui/drawable/DelegateIcon.java +++ b/src/main/java/com/cleanroommc/modularui/drawable/DelegateIcon.java @@ -1,6 +1,5 @@ package com.cleanroommc.modularui.drawable; -import com.cleanroommc.modularui.api.drawable.IDrawable; import com.cleanroommc.modularui.api.drawable.IIcon; import com.cleanroommc.modularui.screen.viewport.GuiContext; import com.cleanroommc.modularui.theme.WidgetTheme; diff --git a/src/main/java/com/cleanroommc/modularui/drawable/TiledUITexture.java b/src/main/java/com/cleanroommc/modularui/drawable/TiledUITexture.java index e5d156882..84ebe7cd3 100644 --- a/src/main/java/com/cleanroommc/modularui/drawable/TiledUITexture.java +++ b/src/main/java/com/cleanroommc/modularui/drawable/TiledUITexture.java @@ -35,4 +35,14 @@ public boolean saveToJson(JsonObject json) { } return true; } + + @Override + protected TiledUITexture copy() { + return new TiledUITexture(location, u0, v0, u1, v1, imageWidth, imageHeight, colorType, nonOpaque); + } + + @Override + public TiledUITexture withColorOverride(int color) { + return (TiledUITexture) super.withColorOverride(color); + } } diff --git a/src/main/java/com/cleanroommc/modularui/drawable/UITexture.java b/src/main/java/com/cleanroommc/modularui/drawable/UITexture.java index 516e123cd..c3b4be738 100644 --- a/src/main/java/com/cleanroommc/modularui/drawable/UITexture.java +++ b/src/main/java/com/cleanroommc/modularui/drawable/UITexture.java @@ -48,6 +48,8 @@ static UITexture icon(String name, int x, int y) { @Nullable public final ColorType colorType; public final boolean nonOpaque; + private int colorOverride = 0; + /** * Creates a drawable texture * @@ -164,11 +166,7 @@ public void drawSubArea(float x, float y, float width, float height, float uStar } public void drawSubArea(float x, float y, float width, float height, float uStart, float vStart, float uEnd, float vEnd, WidgetTheme widgetTheme) { - if (canApplyTheme()) { - Color.setGlColor(widgetTheme.getColor()); - } else { - Color.setGlColorOpaque(Color.WHITE.main); - } + applyColor(this.colorType != null ? this.colorType.getColor(widgetTheme) : ColorType.DEFAULT.getColor(widgetTheme)); GuiDraw.drawTexture(this.location, x, y, x + width, y + height, lerpU(uStart), lerpV(vStart), lerpU(uEnd), lerpV(vEnd), this.nonOpaque); } @@ -177,6 +175,15 @@ public boolean canApplyTheme() { return colorType != null; } + @Override + public void applyColor(int themeColor) { + if (this.colorOverride != 0) { + Color.setGlColor(this.colorOverride); + } else { + IDrawable.super.applyColor(themeColor); + } + } + public static UITexture parseFromJson(JsonObject json) { String name = JsonHelper.getString(json, null, "name", "id"); if (name != null) { @@ -218,6 +225,8 @@ public static UITexture parseFromJson(JsonObject json) { } else if (JsonHelper.getBoolean(json, false, "canApplyTheme")) { builder.canApplyTheme(); } + UITexture uiTexture = builder.build(); + uiTexture.colorOverride = JsonHelper.getColor(json, 0, "colorOverride"); return builder.build(); } @@ -234,9 +243,20 @@ public boolean saveToJson(JsonObject json) { json.addProperty("u1", this.u1); json.addProperty("v1", this.v1); if (this.colorType != null) json.addProperty("colorType", this.colorType.getName()); + json.addProperty("colorOverride", this.colorOverride); return true; } + protected UITexture copy() { + return new UITexture(this.location, this.u0, this.v0, this.u1, this.v1, this.colorType); + } + + public UITexture withColorOverride(int color) { + UITexture t = copy(); + t.colorOverride = color; + return t; + } + private static int defaultImageWidth = 16, defaultImageHeight = 16; public static void setDefaultImageSize(int w, int h) { From cd3e5e444d58191658dfc396cfb8db2fc9198030 Mon Sep 17 00:00:00 2001 From: brachy84 Date: Fri, 2 Jan 2026 10:18:24 +0100 Subject: [PATCH 42/50] make adding bogo sort buttons much easier (cherry picked from commit 558eaea3e00566e6abd6d81ee3c46df79697b154) --- .../cleanroommc/modularui/test/TestTile.java | 22 ++-- .../modularui/value/sync/ISyncRegistrar.java | 8 +- .../modularui/widgets/SlotGroupWidget.java | 123 +++++++++++++++++- .../widgets/slot/PlayerSlotGroup.java | 39 ++++++ .../modularui/widgets/slot/SlotGroup.java | 4 + 5 files changed, 177 insertions(+), 19 deletions(-) create mode 100644 src/main/java/com/cleanroommc/modularui/widgets/slot/PlayerSlotGroup.java diff --git a/src/main/java/com/cleanroommc/modularui/test/TestTile.java b/src/main/java/com/cleanroommc/modularui/test/TestTile.java index 0d24d5a6c..4fdc3c04b 100644 --- a/src/main/java/com/cleanroommc/modularui/test/TestTile.java +++ b/src/main/java/com/cleanroommc/modularui/test/TestTile.java @@ -364,15 +364,16 @@ public ModularPanel buildUI(PosGuiData guiData, PanelSyncManager syncManager, UI .childPadding(2) //.child(SlotGroupWidget.playerInventory().left(0)) .child(SlotGroupWidget.builder() - .matrix("III", "III", "III") - .key('I', index -> { - // 4 is the middle slot with a negative priority -> shift click prioritises middle slot - if (index == 4) { - return new ItemSlot().slot(SyncHandlers.itemSlot(this.bigInventory, index).singletonSlotGroup(-100)); - } - return new ItemSlot().slot(SyncHandlers.itemSlot(this.bigInventory, index).slotGroup("item_inv")); - }) - .build().name("9 slot inv") + .matrix("III", "III", "III") + .key('I', index -> { + // 4 is the middle slot with a negative priority -> shift click prioritises middle slot + if (index == 4) { + return new ItemSlot().slot(SyncHandlers.itemSlot(this.bigInventory, index).singletonSlotGroup(-100)); + } + return new ItemSlot().slot(SyncHandlers.itemSlot(this.bigInventory, index).slotGroup("item_inv")); + }) + .build().name("9 slot inv") + .placeSortButtonsTopRightVertical() //.marginBottom(2) ) .child(SlotGroupWidget.builder() @@ -380,7 +381,8 @@ public ModularPanel buildUI(PosGuiData guiData, PanelSyncManager syncManager, UI .row("FII") .key('F', index -> new FluidSlot().syncHandler("mixer_fluids", index)) .key('I', index -> ItemSlot.create(index >= 2).slot(new ModularSlot(this.mixerItems, index).slotGroup("mixer_items"))) - .build().name("mixer inv")) + .build().name("mixer inv") + .disableSortButtons()) .child(new Row() .coverChildrenHeight() .child(new CycleButtonWidget() diff --git a/src/main/java/com/cleanroommc/modularui/value/sync/ISyncRegistrar.java b/src/main/java/com/cleanroommc/modularui/value/sync/ISyncRegistrar.java index 38717ea7a..00eaa6e99 100644 --- a/src/main/java/com/cleanroommc/modularui/value/sync/ISyncRegistrar.java +++ b/src/main/java/com/cleanroommc/modularui/value/sync/ISyncRegistrar.java @@ -4,6 +4,7 @@ import com.cleanroommc.modularui.api.ISyncedAction; import com.cleanroommc.modularui.utils.item.PlayerMainInvWrapper; import com.cleanroommc.modularui.widgets.slot.ModularSlot; +import com.cleanroommc.modularui.widgets.slot.PlayerSlotGroup; import com.cleanroommc.modularui.widgets.slot.SlotGroup; import net.minecraft.entity.player.EntityPlayer; @@ -82,16 +83,15 @@ default S bindPlayerInventory(EntityPlayer player) { } default S bindPlayerInventory(EntityPlayer player, @NotNull PanelSyncManager.SlotFunction slotFunction) { - if (getSlotGroup(ModularSyncManager.PLAYER_INVENTORY) != null) { + if (getSlotGroup(PlayerSlotGroup.NAME) != null) { throw new IllegalStateException("The player slot group is already registered!"); } PlayerMainInvWrapper playerInventory = new PlayerMainInvWrapper(player.inventory); String key = "player"; for (int i = 0; i < 36; i++) { - itemSlot(key, i, slotFunction.apply(playerInventory, i).slotGroup(ModularSyncManager.PLAYER_INVENTORY)); + itemSlot(key, i, slotFunction.apply(playerInventory, i).slotGroup(PlayerSlotGroup.NAME)); } - // player inv sorting is handled by bogosorter - registerSlotGroup(new SlotGroup(ModularSyncManager.PLAYER_INVENTORY, 9, SlotGroup.PLAYER_INVENTORY_PRIO, true).setAllowSorting(false)); + registerSlotGroup(new PlayerSlotGroup(PlayerSlotGroup.NAME)); return (S) this; } diff --git a/src/main/java/com/cleanroommc/modularui/widgets/SlotGroupWidget.java b/src/main/java/com/cleanroommc/modularui/widgets/SlotGroupWidget.java index dded9a213..6ec403d4e 100644 --- a/src/main/java/com/cleanroommc/modularui/widgets/SlotGroupWidget.java +++ b/src/main/java/com/cleanroommc/modularui/widgets/SlotGroupWidget.java @@ -4,6 +4,7 @@ import com.cleanroommc.modularui.api.widget.IWidget; import com.cleanroommc.modularui.widget.ParentWidget; import com.cleanroommc.modularui.widgets.slot.ItemSlot; +import com.cleanroommc.modularui.widgets.slot.SlotGroup; import it.unimi.dsi.fastutil.chars.Char2IntMap; import it.unimi.dsi.fastutil.chars.Char2IntOpenHashMap; @@ -13,6 +14,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.function.Consumer; import java.util.function.IntFunction; public class SlotGroupWidget extends ParentWidget { @@ -57,15 +59,66 @@ public static SlotGroupWidget playerInventory(SlotConsumer slotConsumer) { return slotGroupWidget; } - public interface SlotConsumer { + private String slotGroupName; + private SlotGroup slotGroup; + private boolean sortButtonsAdded = false; + private Consumer sortButtonsEditor; - ItemSlot apply(int index, ItemSlot widgetSlot); + @Override + public void onInit() { + super.onInit(); + if (!this.sortButtonsAdded) { + SortButtons sb = new SortButtons(); + if (this.sortButtonsEditor == null) placeSortButtonsTopRightHorizontal(); + if (getName() != null) { + sb.name(getName() + "_sorter_buttons"); + } + child(sb); + } + } + + @Override + public void afterInit() { + super.afterInit(); + if (this.slotGroup != null) { + for (IWidget widget : getChildren()) { + if (widget instanceof ItemSlot itemSlot) { + itemSlot.getSlot().slotGroup(this.slotGroup); + } + } + } else if (this.slotGroupName != null) { + for (IWidget widget : getChildren()) { + if (widget instanceof ItemSlot itemSlot) { + itemSlot.getSlot().slotGroup(this.slotGroupName); + } + } + } } - private String slotsKeyName; + @Override + protected void onChildAdd(IWidget child) { + super.onChildAdd(child); + if (child instanceof SortButtons sortButtons) { + this.sortButtonsAdded = true; + if (sortButtons.getSlotGroup() == null && sortButtons.getSlotGroupName() == null) { + if (this.slotGroup != null) { + sortButtons.slotGroup(this.slotGroup); + } else if (this.slotGroupName != null) { + sortButtons.slotGroup(this.slotGroupName); + } + } + if (this.sortButtonsEditor != null) { + this.sortButtonsEditor.accept(sortButtons); + } + } + } + + public SlotGroupWidget disableSortButtons() { + this.sortButtonsAdded = true; + return this; + } public void setSlotsSynced(String name) { - this.slotsKeyName = name; int i = 0; for (IWidget widget : getChildren()) { if (widget instanceof ISynced synced) { @@ -75,15 +128,59 @@ public void setSlotsSynced(String name) { } } + public SlotGroupWidget editSortButtons(Consumer sortButtonsEditor) { + this.sortButtonsEditor = sortButtonsEditor; + return this; + } + + public SlotGroupWidget placeSortButtonsTopRightVertical() { + return placeSortButtonsTopRightVertical(this.sortButtonsEditor); + } + + public SlotGroupWidget placeSortButtonsTopRightHorizontal() { + return placeSortButtonsTopRightHorizontal(this.sortButtonsEditor); + } + + public SlotGroupWidget placeSortButtonsTopRightVertical(Consumer additionalEdits) { + return editSortButtons(sb -> { + sb.vertical().leftRelOffset(1f, 1).top(0); + if (additionalEdits != null) additionalEdits.accept(sb); + }); + } + + public SlotGroupWidget placeSortButtonsTopRightHorizontal(Consumer additionalEdits) { + return editSortButtons(sb -> { + sb.horizontal().bottomRelOffset(1f, 1).right(0); + if (additionalEdits != null) additionalEdits.accept(sb); + }); + } + + public SlotGroupWidget slotGroup(String slotGroupName) { + this.slotGroupName = slotGroupName; + return this; + } + + public SlotGroupWidget slotGroup(SlotGroup slotGroup) { + this.slotGroup = slotGroup; + return this; + } + public static Builder builder() { return new Builder(); } + public interface SlotConsumer { + + ItemSlot apply(int index, ItemSlot widgetSlot); + } + public static class Builder { private String syncKey; private final List matrix = new ArrayList<>(); private final Char2ObjectMap keys = new Char2ObjectOpenHashMap<>(); + private String slotGroupName; + private SlotGroup slotGroup; private Builder() { this.keys.put(' ', null); @@ -115,8 +212,20 @@ public Builder key(char c, IntFunction widget) { return this; } + public Builder slotGroup(String slotGroupName) { + this.slotGroupName = slotGroupName; + return this; + } + + public Builder slotGroup(SlotGroup slotGroup) { + this.slotGroup = slotGroup; + return this; + } + public SlotGroupWidget build() { - SlotGroupWidget slotGroupWidget = new SlotGroupWidget(); + SlotGroupWidget slotGroupWidget = new SlotGroupWidget() + .slotGroup(this.slotGroupName) + .slotGroup(this.slotGroup); Char2IntMap charCount = new Char2IntOpenHashMap(); int x = 0, y = 0, maxWidth = 0; int syncId = 0; @@ -129,6 +238,10 @@ public SlotGroupWidget build() { IWidget widget; if (o instanceof IWidget iWidget) { widget = iWidget; + if (count > 0) { + throw new IllegalArgumentException("A widget can only exist once in the widget tree, but the char '" + c + + "' exists more than once in this slot group widget and it has a static widget supplied."); + } } else if (o instanceof IntFunction function) { widget = (IWidget) function.apply(count); } else { diff --git a/src/main/java/com/cleanroommc/modularui/widgets/slot/PlayerSlotGroup.java b/src/main/java/com/cleanroommc/modularui/widgets/slot/PlayerSlotGroup.java new file mode 100644 index 000000000..d0b491e05 --- /dev/null +++ b/src/main/java/com/cleanroommc/modularui/widgets/slot/PlayerSlotGroup.java @@ -0,0 +1,39 @@ +package com.cleanroommc.modularui.widgets.slot; + +import net.minecraft.inventory.Slot; + +public class PlayerSlotGroup extends SlotGroup { + + public static final String NAME = "player_inventory"; + + private Slot mainInvSlot; + + public PlayerSlotGroup(String name) { + super(name, 9, PLAYER_INVENTORY_PRIO, true); + } + + @Override + public Slot getFirstSlotForSorting() { + // we need to return the first non hotbar slot + // in mui the main inv and the hotbar is a single slot group + // in bogo they are separated + // this method is also only used in the sort buttons, which are added to the main inv and not the hotbar + if (mainInvSlot == null) { + for (Slot slot : getSlots()) { + if (slot.getSlotIndex() >= 9 && slot.getSlotIndex() < 36) { + mainInvSlot = slot; + break; + } + } + } + return mainInvSlot; + } + + @Override + void removeSlot(ModularSlot slot) { + super.removeSlot(slot); + if (mainInvSlot == slot) { + mainInvSlot = null; + } + } +} diff --git a/src/main/java/com/cleanroommc/modularui/widgets/slot/SlotGroup.java b/src/main/java/com/cleanroommc/modularui/widgets/slot/SlotGroup.java index 57ff79b45..a70eac567 100644 --- a/src/main/java/com/cleanroommc/modularui/widgets/slot/SlotGroup.java +++ b/src/main/java/com/cleanroommc/modularui/widgets/slot/SlotGroup.java @@ -89,6 +89,10 @@ public List getSlots() { return Collections.unmodifiableList(this.slots); } + public Slot getFirstSlotForSorting() { + return this.slots.isEmpty() ? null : this.slots.get(0); + } + public int getRowSize() { return this.rowSize; } From 061d7071e7fdd4e4fd116558bcdb36f8848118ce Mon Sep 17 00:00:00 2001 From: brachy84 Date: Fri, 2 Jan 2026 10:19:25 +0100 Subject: [PATCH 43/50] rework syncing so inactive screens can still receive packets (cherry picked from commit 33d41a943d01b361d6b5d9d15ca23b038ce078ab) --- .../modularui/factory/GuiManager.java | 3 + .../modularui/network/ModularNetwork.java | 98 +++++++++++++++++++ .../modularui/network/NetworkUtils.java | 10 +- .../network/packets/OpenGuiPacket.java | 12 +-- .../network/packets/PacketSyncHandler.java | 44 +++------ .../modularui/screen/ModularContainer.java | 2 + .../value/sync/ModularSyncManager.java | 5 +- .../value/sync/PanelSyncManager.java | 11 +-- .../modularui/value/sync/SyncHandler.java | 30 +++--- 9 files changed, 148 insertions(+), 67 deletions(-) create mode 100644 src/main/java/com/cleanroommc/modularui/network/ModularNetwork.java diff --git a/src/main/java/com/cleanroommc/modularui/factory/GuiManager.java b/src/main/java/com/cleanroommc/modularui/factory/GuiManager.java index 59204ed81..126290d82 100644 --- a/src/main/java/com/cleanroommc/modularui/factory/GuiManager.java +++ b/src/main/java/com/cleanroommc/modularui/factory/GuiManager.java @@ -4,6 +4,7 @@ import com.cleanroommc.modularui.api.MCHelper; import com.cleanroommc.modularui.api.RecipeViewerSettings; import com.cleanroommc.modularui.api.UIFactory; +import com.cleanroommc.modularui.network.ModularNetwork; import com.cleanroommc.modularui.network.NetworkHandler; import com.cleanroommc.modularui.network.packets.OpenGuiPacket; import com.cleanroommc.modularui.screen.GuiContainerWrapper; @@ -85,6 +86,7 @@ public static void open(@NotNull UIFactory factory, @NotN int windowId = player.currentWindowId; PacketBuffer buffer = new PacketBuffer(Unpooled.buffer()); factory.writeGuiData(guiData, buffer); + ModularNetwork.SERVER.activate(windowId, msm); NetworkHandler.sendToPlayer(new OpenGuiPacket<>(windowId, factory, buffer), player); // open container // this mimics forge behaviour player.openContainer = container; @@ -113,6 +115,7 @@ public static void openFromClient(int windowId, @NotNull UIF } if (guiContainer.inventorySlots != container) throw new IllegalStateException("Custom Containers are not yet allowed!"); guiContainer.inventorySlots.windowId = windowId; + ModularNetwork.CLIENT.activate(windowId, msm); MCHelper.displayScreen(wrapper.getGuiScreen()); player.openContainer = guiContainer.inventorySlots; syncManager.onOpen(); // TODO: not here in 1.12 diff --git a/src/main/java/com/cleanroommc/modularui/network/ModularNetwork.java b/src/main/java/com/cleanroommc/modularui/network/ModularNetwork.java new file mode 100644 index 000000000..f951f30ba --- /dev/null +++ b/src/main/java/com/cleanroommc/modularui/network/ModularNetwork.java @@ -0,0 +1,98 @@ +package com.cleanroommc.modularui.network; + +import com.cleanroommc.modularui.ModularUI; +import com.cleanroommc.modularui.network.packets.PacketSyncHandler; +import com.cleanroommc.modularui.value.sync.ModularSyncManager; +import com.cleanroommc.modularui.value.sync.SyncHandler; + +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.entity.player.EntityPlayerMP; +import net.minecraft.network.PacketBuffer; +import net.minecraftforge.fml.relauncher.Side; +import net.minecraftforge.fml.relauncher.SideOnly; + +import it.unimi.dsi.fastutil.ints.Int2ReferenceOpenHashMap; +import it.unimi.dsi.fastutil.objects.Reference2IntOpenHashMap; + +import java.io.IOException; + +public abstract class ModularNetwork { + + // These need to be separate instances, otherwise they would access the same maps in singleplayer. + @SideOnly(Side.CLIENT) + public static final ModularNetwork CLIENT = new ModularNetwork(true) { + @Override + protected void sendPacket(IPacket packet, EntityPlayer player) { + NetworkHandler.sendToServer(packet); + } + }; + public static final ModularNetwork SERVER = new ModularNetwork(false) { + @Override + protected void sendPacket(IPacket packet, EntityPlayer player) { + NetworkHandler.sendToPlayer(packet, (EntityPlayerMP) player); + } + }; + + public static ModularNetwork get(boolean client) { + return client ? CLIENT : SERVER; + } + + public static ModularNetwork get(Side side) { + return side.isClient() ? CLIENT : SERVER; + } + + public static ModularNetwork get(EntityPlayer player) { + return get(NetworkUtils.isClient(player)); + } + + private final boolean client; + private final Int2ReferenceOpenHashMap activeScreens = new Int2ReferenceOpenHashMap<>(); + private final Reference2IntOpenHashMap inverseActiveScreens = new Reference2IntOpenHashMap<>(); + + private ModularNetwork(boolean client) { + this.client = client; + } + + public boolean isClient() { + return client; + } + + protected abstract void sendPacket(IPacket packet, EntityPlayer player); + + public void activate(int networkId, ModularSyncManager manager) { + if (activeScreens.containsKey(networkId)) throw new IllegalStateException("Network ID " + networkId + " is already active."); + activeScreens.put(networkId, manager); + inverseActiveScreens.put(manager, networkId); + } + + public void deactivate(ModularSyncManager manager) { + int id = inverseActiveScreens.removeInt(manager); + activeScreens.remove(id); + } + + public void receivePacket(PacketSyncHandler packet) { + ModularSyncManager msm = activeScreens.get(packet.networkId); + if (msm == null) return; // silently discard packets for inactive screens + try { + int id = packet.action ? 0 : packet.packet.readVarInt(); + msm.receiveWidgetUpdate(packet.panel, packet.key, packet.action, id, packet.packet); + } catch (IndexOutOfBoundsException e) { + ModularUI.LOGGER.error("Failed to read packet for sync handler {} in panel {}", packet.key, packet.panel); + } catch (IOException e) { + ModularUI.LOGGER.throwing(e); + } + } + + public void sendSyncHandlerPacket(String panel, SyncHandler syncHandler, PacketBuffer buffer, EntityPlayer player) { + ModularSyncManager msm = syncHandler.getSyncManager().getModularSyncManager(); + if (!inverseActiveScreens.containsKey(msm)) return; + int id = inverseActiveScreens.getInt(msm); + sendPacket(new PacketSyncHandler(id, panel, syncHandler.getKey(), false, buffer), player); + } + + public void sendActionPacket(ModularSyncManager msm, String panel, String key, PacketBuffer buffer, EntityPlayer player) { + if (!inverseActiveScreens.containsKey(msm)) return; + int id = inverseActiveScreens.getInt(msm); + sendPacket(new PacketSyncHandler(id, panel, key, true, buffer), player); + } +} diff --git a/src/main/java/com/cleanroommc/modularui/network/NetworkUtils.java b/src/main/java/com/cleanroommc/modularui/network/NetworkUtils.java index f7e7fb2a4..9d6cd0f0e 100644 --- a/src/main/java/com/cleanroommc/modularui/network/NetworkUtils.java +++ b/src/main/java/com/cleanroommc/modularui/network/NetworkUtils.java @@ -12,6 +12,7 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; +import org.apache.commons.lang3.StringUtils; import org.jetbrains.annotations.Nullable; import java.io.IOException; @@ -115,6 +116,10 @@ public static void writeStringSafe(PacketBuffer buffer, @Nullable String string, buffer.writeVarIntToBuffer(Short.MAX_VALUE + 1); return; } + if (string.isEmpty()) { + buffer.writeVarIntToBuffer(0); + return; + } byte[] bytesTest = string.getBytes(StandardCharsets.UTF_8); byte[] bytes; @@ -134,9 +139,8 @@ public static void writeStringSafe(PacketBuffer buffer, @Nullable String string, public static String readStringSafe(PacketBuffer buffer) { int length = buffer.readVarIntFromBuffer(); - if (length > Short.MAX_VALUE) { - return null; - } + if (length > Short.MAX_VALUE) return null; + if (length == 0) return StringUtils.EMPTY; String s = buffer.toString(buffer.readerIndex(), length, StandardCharsets.UTF_8); buffer.readerIndex(buffer.readerIndex() + length); return s; diff --git a/src/main/java/com/cleanroommc/modularui/network/packets/OpenGuiPacket.java b/src/main/java/com/cleanroommc/modularui/network/packets/OpenGuiPacket.java index 44a3c1014..96e63f1e8 100644 --- a/src/main/java/com/cleanroommc/modularui/network/packets/OpenGuiPacket.java +++ b/src/main/java/com/cleanroommc/modularui/network/packets/OpenGuiPacket.java @@ -19,28 +19,28 @@ public class OpenGuiPacket implements IPacket { - private int windowId; + private int networkId; private UIFactory factory; private PacketBuffer data; public OpenGuiPacket() {} - public OpenGuiPacket(int windowId, UIFactory factory, PacketBuffer data) { - this.windowId = windowId; + public OpenGuiPacket(int networkId, UIFactory factory, PacketBuffer data) { + this.networkId = networkId; this.factory = factory; this.data = data; } @Override public void write(PacketBuffer buf) throws IOException { - buf.writeVarIntToBuffer(this.windowId); + buf.writeVarIntToBuffer(this.networkId); NetworkUtils.writeStringSafe(buf, this.factory.getFactoryName()); NetworkUtils.writeByteBuf(buf, this.data); } @Override public void read(PacketBuffer buf) { - this.windowId = buf.readVarIntFromBuffer(); + this.networkId = buf.readVarIntFromBuffer(); this.factory = (UIFactory) GuiManager.getFactory(NetworkUtils.readStringSafe(buf)); this.data = NetworkUtils.readPacketBuffer(buf); } @@ -48,7 +48,7 @@ public void read(PacketBuffer buf) { @SideOnly(Side.CLIENT) @Override public @Nullable IPacket executeClient(NetHandlerPlayClient handler) { - GuiManager.openFromClient(this.windowId, this.factory, this.data, Platform.getClientPlayer()); + GuiManager.openFromClient(this.networkId, this.factory, this.data, Platform.getClientPlayer()); return null; } diff --git a/src/main/java/com/cleanroommc/modularui/network/packets/PacketSyncHandler.java b/src/main/java/com/cleanroommc/modularui/network/packets/PacketSyncHandler.java index 3163edfda..1b2605036 100644 --- a/src/main/java/com/cleanroommc/modularui/network/packets/PacketSyncHandler.java +++ b/src/main/java/com/cleanroommc/modularui/network/packets/PacketSyncHandler.java @@ -1,31 +1,32 @@ package com.cleanroommc.modularui.network.packets; -import com.cleanroommc.modularui.ModularUI; import com.cleanroommc.modularui.network.IPacket; +import com.cleanroommc.modularui.network.ModularNetwork; import com.cleanroommc.modularui.network.NetworkUtils; import com.cleanroommc.modularui.screen.ModularContainer; import com.cleanroommc.modularui.screen.ModularScreen; import com.cleanroommc.modularui.value.sync.ModularSyncManager; import net.minecraft.client.network.NetHandlerPlayClient; -import net.minecraft.inventory.Container; import net.minecraft.network.NetHandlerPlayServer; import net.minecraft.network.PacketBuffer; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.Nullable; -import java.io.IOException; - +@ApiStatus.Internal public class PacketSyncHandler implements IPacket { - private String panel; - private String key; - private boolean action; - private PacketBuffer packet; + public int networkId; + public String panel; + public String key; + public boolean action; + public PacketBuffer packet; public PacketSyncHandler() {} - public PacketSyncHandler(String panel, String key, boolean action, PacketBuffer packet) { + public PacketSyncHandler(int networkId, String panel, String key, boolean action, PacketBuffer packet) { + this.networkId = networkId; this.panel = panel; this.key = key; this.action = action; @@ -34,7 +35,8 @@ public PacketSyncHandler(String panel, String key, boolean action, PacketBuffer @Override public void write(PacketBuffer buf) { - NetworkUtils.writeStringSafe(buf, this.panel); + buf.writeVarIntToBuffer(this.networkId); + NetworkUtils.writeStringSafe(buf, this.panel, 256, true); NetworkUtils.writeStringSafe(buf, this.key, 256, true); buf.writeBoolean(this.action); NetworkUtils.writeByteBuf(buf, this.packet); @@ -42,6 +44,7 @@ public void write(PacketBuffer buf) { @Override public void read(PacketBuffer buf) { + this.networkId = buf.readVarIntFromBuffer(); this.panel = NetworkUtils.readStringSafe(buf); this.key = NetworkUtils.readStringSafe(buf); this.action = buf.readBoolean(); @@ -50,30 +53,13 @@ public void read(PacketBuffer buf) { @Override public @Nullable IPacket executeClient(NetHandlerPlayClient handler) { - ModularScreen screen = ModularScreen.getCurrent(); - if (screen != null) { - execute(screen.getSyncManager()); - } + ModularNetwork.CLIENT.receivePacket(this); return null; } @Override public @Nullable IPacket executeServer(NetHandlerPlayServer handler) { - Container container = handler.playerEntity.openContainer; - if (container instanceof ModularContainer modularContainer) { - execute(modularContainer.getSyncManager()); - } + ModularNetwork.SERVER.receivePacket(this); return null; } - - private void execute(ModularSyncManager syncManager) { - try { - int id = this.action ? 0 : this.packet.readVarIntFromBuffer(); - syncManager.receiveWidgetUpdate(this.panel, this.key, this.action, id, this.packet); - } catch (IndexOutOfBoundsException e) { - ModularUI.LOGGER.error("Failed to read packet for sync handler {} in panel {}", this.key, this.panel); - } catch (IOException e) { - ModularUI.LOGGER.throwing(e); - } - } } diff --git a/src/main/java/com/cleanroommc/modularui/screen/ModularContainer.java b/src/main/java/com/cleanroommc/modularui/screen/ModularContainer.java index 0b73e45ea..47bdfd84a 100644 --- a/src/main/java/com/cleanroommc/modularui/screen/ModularContainer.java +++ b/src/main/java/com/cleanroommc/modularui/screen/ModularContainer.java @@ -4,6 +4,7 @@ import com.cleanroommc.modularui.api.inventory.ClickType; import com.cleanroommc.modularui.core.mixins.early.minecraft.ContainerAccessor; import com.cleanroommc.modularui.factory.GuiData; +import com.cleanroommc.modularui.network.ModularNetwork; import com.cleanroommc.modularui.network.NetworkUtils; import com.cleanroommc.modularui.utils.Platform; import com.cleanroommc.modularui.utils.item.ItemHandlerHelper; @@ -113,6 +114,7 @@ public void onModularContainerOpened() { public void onModularContainerClosed() { if (this.syncManager != null) { this.syncManager.dispose(); + ModularNetwork.get(this.player).deactivate(this.syncManager); } } diff --git a/src/main/java/com/cleanroommc/modularui/value/sync/ModularSyncManager.java b/src/main/java/com/cleanroommc/modularui/value/sync/ModularSyncManager.java index 2b16df77f..b1ecd081e 100644 --- a/src/main/java/com/cleanroommc/modularui/value/sync/ModularSyncManager.java +++ b/src/main/java/com/cleanroommc/modularui/value/sync/ModularSyncManager.java @@ -8,6 +8,7 @@ import com.cleanroommc.modularui.utils.item.PlayerInvWrapper; import com.cleanroommc.modularui.utils.item.PlayerMainInvWrapper; import com.cleanroommc.modularui.utils.item.SlotItemHandler; +import com.cleanroommc.modularui.widgets.slot.PlayerSlotGroup; import com.cleanroommc.modularui.widgets.slot.SlotGroup; import net.minecraft.entity.player.EntityPlayer; @@ -29,7 +30,7 @@ public class ModularSyncManager implements ISyncRegistrar { public static final String AUTO_SYNC_PREFIX = "auto_sync:"; - protected static final String PLAYER_INVENTORY = "player_inventory"; + private static final String CURSOR_KEY = ISyncRegistrar.makeSyncKey("cursor_slot", 255255); private final Map panelSyncManagerMap = new Object2ObjectOpenHashMap<>(); @@ -52,7 +53,7 @@ void setMainPSM(PanelSyncManager mainPSM) { @ApiStatus.Internal public void construct(ModularContainer container, String mainPanelName) { this.container = container; - if (this.mainPSM.getSlotGroup(PLAYER_INVENTORY) == null) { + if (this.mainPSM.getSlotGroup(PlayerSlotGroup.NAME) == null) { this.mainPSM.bindPlayerInventory(getPlayer()); } this.mainPSM.syncValue(CURSOR_KEY, this.cursorSlotSyncHandler); diff --git a/src/main/java/com/cleanroommc/modularui/value/sync/PanelSyncManager.java b/src/main/java/com/cleanroommc/modularui/value/sync/PanelSyncManager.java index e5a01d0cb..6668774b1 100644 --- a/src/main/java/com/cleanroommc/modularui/value/sync/PanelSyncManager.java +++ b/src/main/java/com/cleanroommc/modularui/value/sync/PanelSyncManager.java @@ -3,15 +3,13 @@ import com.cleanroommc.modularui.ModularUI; import com.cleanroommc.modularui.api.IPanelHandler; import com.cleanroommc.modularui.api.ISyncedAction; -import com.cleanroommc.modularui.network.NetworkHandler; -import com.cleanroommc.modularui.network.packets.PacketSyncHandler; +import com.cleanroommc.modularui.network.ModularNetwork; import com.cleanroommc.modularui.screen.ModularContainer; import com.cleanroommc.modularui.utils.item.PlayerMainInvWrapper; import com.cleanroommc.modularui.widgets.slot.ModularSlot; import com.cleanroommc.modularui.widgets.slot.SlotGroup; import net.minecraft.entity.player.EntityPlayer; -import net.minecraft.entity.player.EntityPlayerMP; import net.minecraft.item.ItemStack; import net.minecraft.network.PacketBuffer; import cpw.mods.fml.relauncher.Side; @@ -325,12 +323,7 @@ public PanelSyncManager registerSyncedAction(String mapKey, boolean executeClien public void callSyncedAction(String mapKey, PacketBuffer packet) { if (invokeSyncedAction(mapKey, packet)) { - PacketSyncHandler packetSyncHandler = new PacketSyncHandler(this.panelName, mapKey, true, packet); - if (isClient()) { - NetworkHandler.sendToServer(packetSyncHandler); - } else { - NetworkHandler.sendToPlayer(packetSyncHandler, (EntityPlayerMP) getPlayer()); - } + ModularNetwork.get(isClient()).sendActionPacket(getModularSyncManager(), this.panelName, mapKey, packet, getPlayer()); } } diff --git a/src/main/java/com/cleanroommc/modularui/value/sync/SyncHandler.java b/src/main/java/com/cleanroommc/modularui/value/sync/SyncHandler.java index 7643b5eca..47f2355f9 100644 --- a/src/main/java/com/cleanroommc/modularui/value/sync/SyncHandler.java +++ b/src/main/java/com/cleanroommc/modularui/value/sync/SyncHandler.java @@ -2,10 +2,8 @@ import com.cleanroommc.modularui.api.IPacketWriter; import com.cleanroommc.modularui.api.value.ISyncOrValue; -import com.cleanroommc.modularui.network.NetworkHandler; -import com.cleanroommc.modularui.network.packets.PacketSyncHandler; +import com.cleanroommc.modularui.network.ModularNetwork; -import net.minecraft.entity.player.EntityPlayerMP; import net.minecraft.network.PacketBuffer; import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; @@ -91,11 +89,7 @@ public final void sync(int id, @NotNull IPacketWriter bufferConsumer) { } catch (IOException e) { throw new RuntimeException(e); } - if (getSyncManager().isClient()) { - sendToServer(getSyncManager().getPanelName(), buffer, this); - } else { - sendToClient(getSyncManager().getPanelName(), buffer, this); - } + send(ModularNetwork.get(getSyncManager().isClient()), getSyncManager().getPanelName(), buffer, this); } /** @@ -162,7 +156,7 @@ public final String getKey() { } /** - * @return is this sync handler has been initialised yet + * @return is this sync handler has been initialized yet */ public final boolean isValid() { return this.key != null && this.syncManager != null; @@ -187,21 +181,21 @@ public boolean isSyncHandler() { return true; } - public static void sendToClient(String panel, PacketBuffer buffer, SyncHandler syncHandler) { + private static void send(ModularNetwork network, String panel, PacketBuffer buffer, SyncHandler syncHandler) { Objects.requireNonNull(buffer); Objects.requireNonNull(syncHandler); if (!syncHandler.isValid()) { - throw new IllegalStateException(); + throw new IllegalStateException("Not initialized sync handlers can't send packets!"); } - NetworkHandler.sendToPlayer(new PacketSyncHandler(panel, syncHandler.getKey(), false, buffer), (EntityPlayerMP) syncHandler.syncManager.getPlayer()); + network.sendSyncHandlerPacket(panel, syncHandler, buffer, syncHandler.syncManager.getPlayer()); + } + + public static void sendToClient(String panel, PacketBuffer buffer, SyncHandler syncHandler) { + send(ModularNetwork.SERVER, panel, buffer, syncHandler); } + @SideOnly(Side.CLIENT) public static void sendToServer(String panel, PacketBuffer buffer, SyncHandler syncHandler) { - Objects.requireNonNull(buffer); - Objects.requireNonNull(syncHandler); - if (!syncHandler.isValid()) { - throw new IllegalStateException(); - } - NetworkHandler.sendToServer(new PacketSyncHandler(panel, syncHandler.getKey(), false, buffer)); + send(ModularNetwork.CLIENT, panel, buffer, syncHandler); } } From ea627e55e4594cce05451e95d14a4d28e5af3f22 Mon Sep 17 00:00:00 2001 From: brachy84 Date: Fri, 2 Jan 2026 23:23:11 +0100 Subject: [PATCH 44/50] experimental container stack (cherry picked from commit e982a6f016004c2afba5e075f42e5033cf0eb21f) --- .../cleanroommc/modularui/api/MCHelper.java | 25 ++-- .../modularui/factory/GuiManager.java | 18 ++- .../modularui/network/ModularNetwork.java | 125 +++++++++--------- .../modularui/network/ModularNetworkSide.java | 118 +++++++++++++++++ .../modularui/network/NetworkHandler.java | 17 ++- .../network/packets/CloseAllGuiPacket.java | 39 ++++++ .../network/packets/CloseGuiPacket.java | 53 ++++++++ .../network/packets/OpenGuiPacket.java | 8 +- .../network/packets/PacketSyncHandler.java | 3 + .../modularui/network/packets/SClipboard.java | 5 +- .../modularui/screen/ClientScreenHandler.java | 3 + .../modularui/screen/ModularContainer.java | 18 +-- .../value/sync/ModularSyncManager.java | 45 +++++++ .../modularui/value/sync/SyncHandler.java | 3 +- 14 files changed, 372 insertions(+), 108 deletions(-) create mode 100644 src/main/java/com/cleanroommc/modularui/network/ModularNetworkSide.java create mode 100644 src/main/java/com/cleanroommc/modularui/network/packets/CloseAllGuiPacket.java create mode 100644 src/main/java/com/cleanroommc/modularui/network/packets/CloseGuiPacket.java diff --git a/src/main/java/com/cleanroommc/modularui/api/MCHelper.java b/src/main/java/com/cleanroommc/modularui/api/MCHelper.java index 07b8eec26..43ead8323 100644 --- a/src/main/java/com/cleanroommc/modularui/api/MCHelper.java +++ b/src/main/java/com/cleanroommc/modularui/api/MCHelper.java @@ -32,13 +32,15 @@ public class MCHelper { public static boolean hasMc() { - return getMc() != null; + return NetworkUtils.isDedicatedClient() && getMc() != null; } + @SideOnly(Side.CLIENT) public static @Nullable Minecraft getMc() { return Minecraft.getMinecraft(); } + @SideOnly(Side.CLIENT) public static @Nullable EntityPlayer getPlayer() { if (hasMc()) { return getMc().thePlayer; @@ -46,22 +48,19 @@ public static boolean hasMc() { return null; } + @SideOnly(Side.CLIENT) public static boolean closeScreen() { if (!hasMc()) return false; - EntityPlayer player = getPlayer(); - if (player != null) { - player.closeScreen(); - return true; - } Minecraft.getMinecraft().displayGuiScreen(null); return false; } + @SideOnly(Side.CLIENT) public static void popScreen(boolean openParentOnClose, GuiScreen parent) { EntityPlayer player = MCHelper.getPlayer(); if (player != null) { - // container should not just be closed here, however this means the gui stack only works with client only screens (except the root) - // TODO: figure out the necessity of a Container stack + // container should not just be closed here + // instead they are kept in a stack until all screens are closed if (openParentOnClose) { Minecraft.getMinecraft().displayGuiScreen(parent); } else { @@ -74,14 +73,6 @@ public static void popScreen(boolean openParentOnClose, GuiScreen parent) { } @SideOnly(Side.CLIENT) - private static void prepareCloseContainer(EntityPlayer entityPlayer) { - if (entityPlayer instanceof EntityClientPlayerMP clientPlayerMP) { - clientPlayerMP.sendQueue.addToSendQueue(new C0DPacketCloseWindow(clientPlayerMP.openContainer.windowId)); - } - entityPlayer.openContainer = entityPlayer.inventoryContainer; - entityPlayer.inventory.setItemStack(null); - } - public static boolean displayScreen(GuiScreen screen) { Minecraft mc = getMc(); if (mc != null) { @@ -91,11 +82,13 @@ public static boolean displayScreen(GuiScreen screen) { return false; } + @SideOnly(Side.CLIENT) public static GuiScreen getCurrentScreen() { Minecraft mc = getMc(); return mc != null ? mc.currentScreen : null; } + @SideOnly(Side.CLIENT) public static FontRenderer getFontRenderer() { if (hasMc()) return getMc().fontRenderer; return null; diff --git a/src/main/java/com/cleanroommc/modularui/factory/GuiManager.java b/src/main/java/com/cleanroommc/modularui/factory/GuiManager.java index 126290d82..57555c864 100644 --- a/src/main/java/com/cleanroommc/modularui/factory/GuiManager.java +++ b/src/main/java/com/cleanroommc/modularui/factory/GuiManager.java @@ -86,18 +86,20 @@ public static void open(@NotNull UIFactory factory, @NotN int windowId = player.currentWindowId; PacketBuffer buffer = new PacketBuffer(Unpooled.buffer()); factory.writeGuiData(guiData, buffer); - ModularNetwork.SERVER.activate(windowId, msm); - NetworkHandler.sendToPlayer(new OpenGuiPacket<>(windowId, factory, buffer), player); + int nid = ModularNetwork.SERVER.activate(msm); + NetworkHandler.sendToPlayer(new OpenGuiPacket<>(windowId, nid, factory, buffer), player); // open container // this mimics forge behaviour player.openContainer = container; player.openContainer.windowId = windowId; player.openContainer.addCraftingToCrafters(player); - container.onModularContainerOpened(); + // init mui syncer + msm.onOpen(); + // 1.12.2 invokes event here which doesnt exist in 1.7.10 } @ApiStatus.Internal @SideOnly(Side.CLIENT) - public static void openFromClient(int windowId, @NotNull UIFactory factory, @NotNull PacketBuffer data, @NotNull EntityPlayerSP player) { + public static void openFromClient(int windowId, int networkId, @NotNull UIFactory factory, @NotNull PacketBuffer data, @NotNull EntityPlayerSP player) { T guiData = factory.readGuiData(player, data); UISettings settings = new UISettings(); settings.defaultCanInteractWith(factory, guiData); @@ -115,17 +117,19 @@ public static void openFromClient(int windowId, @NotNull UIF } if (guiContainer.inventorySlots != container) throw new IllegalStateException("Custom Containers are not yet allowed!"); guiContainer.inventorySlots.windowId = windowId; - ModularNetwork.CLIENT.activate(windowId, msm); + ModularNetwork.CLIENT.activate(networkId, msm); MCHelper.displayScreen(wrapper.getGuiScreen()); player.openContainer = guiContainer.inventorySlots; - syncManager.onOpen(); // TODO: not here in 1.12 + msm.onOpen(); } @SideOnly(Side.CLIENT) public static void openFromClient(@NotNull UIFactory factory, @NotNull T guiData) { + // notify server to open the gui + // server will send packet back to actually open the gui PacketBuffer buffer = new PacketBuffer(Unpooled.buffer()); factory.writeGuiData(guiData, buffer); - NetworkHandler.sendToServer(new OpenGuiPacket<>(0, factory, buffer)); + NetworkHandler.sendToServer(new OpenGuiPacket<>(0, 0, factory, buffer)); } @SideOnly(Side.CLIENT) diff --git a/src/main/java/com/cleanroommc/modularui/network/ModularNetwork.java b/src/main/java/com/cleanroommc/modularui/network/ModularNetwork.java index f951f30ba..ca9ec46d5 100644 --- a/src/main/java/com/cleanroommc/modularui/network/ModularNetwork.java +++ b/src/main/java/com/cleanroommc/modularui/network/ModularNetwork.java @@ -1,98 +1,97 @@ package com.cleanroommc.modularui.network; -import com.cleanroommc.modularui.ModularUI; -import com.cleanroommc.modularui.network.packets.PacketSyncHandler; import com.cleanroommc.modularui.value.sync.ModularSyncManager; -import com.cleanroommc.modularui.value.sync.SyncHandler; +import net.minecraft.client.Minecraft; +import net.minecraft.client.entity.EntityPlayerSP; import net.minecraft.entity.player.EntityPlayer; import net.minecraft.entity.player.EntityPlayerMP; -import net.minecraft.network.PacketBuffer; +import net.minecraft.item.ItemStack; import net.minecraftforge.fml.relauncher.Side; import net.minecraftforge.fml.relauncher.SideOnly; -import it.unimi.dsi.fastutil.ints.Int2ReferenceOpenHashMap; -import it.unimi.dsi.fastutil.objects.Reference2IntOpenHashMap; - -import java.io.IOException; +import org.jetbrains.annotations.ApiStatus; +@ApiStatus.Experimental public abstract class ModularNetwork { // These need to be separate instances, otherwise they would access the same maps in singleplayer. - @SideOnly(Side.CLIENT) - public static final ModularNetwork CLIENT = new ModularNetwork(true) { - @Override - protected void sendPacket(IPacket packet, EntityPlayer player) { - NetworkHandler.sendToServer(packet); - } - }; - public static final ModularNetwork SERVER = new ModularNetwork(false) { - @Override - protected void sendPacket(IPacket packet, EntityPlayer player) { - NetworkHandler.sendToPlayer(packet, (EntityPlayerMP) player); - } - }; + // You have to make sure you are choosing the logical side you are currently on otherwise you can mess things badly, since + // there is no validation. + public static final Client CLIENT = new Client(); + public static final Server SERVER = new Server(); - public static ModularNetwork get(boolean client) { + public static ModularNetworkSide get(boolean client) { return client ? CLIENT : SERVER; } - public static ModularNetwork get(Side side) { + public static ModularNetworkSide get(Side side) { return side.isClient() ? CLIENT : SERVER; } - public static ModularNetwork get(EntityPlayer player) { + public static ModularNetworkSide get(EntityPlayer player) { return get(NetworkUtils.isClient(player)); } - private final boolean client; - private final Int2ReferenceOpenHashMap activeScreens = new Int2ReferenceOpenHashMap<>(); - private final Reference2IntOpenHashMap inverseActiveScreens = new Reference2IntOpenHashMap<>(); + public static final class Client extends ModularNetworkSide { - private ModularNetwork(boolean client) { - this.client = client; - } + private Client() { + super(true); + } - public boolean isClient() { - return client; - } + public void activate(int nid, ModularSyncManager msm) { + activateInternal(nid, msm); + } - protected abstract void sendPacket(IPacket packet, EntityPlayer player); + @Override + void sendPacket(IPacket packet, EntityPlayer player) { + NetworkHandler.sendToServer(packet); + } - public void activate(int networkId, ModularSyncManager manager) { - if (activeScreens.containsKey(networkId)) throw new IllegalStateException("Network ID " + networkId + " is already active."); - activeScreens.put(networkId, manager); - inverseActiveScreens.put(manager, networkId); - } + @Override + void closeContainer(EntityPlayer player) { + // mimics EntityPlayerSP.closeScreenAndDropStack() but without closing the screen + player.inventory.setItemStack(ItemStack.EMPTY); + player.openContainer = player.inventoryContainer; + } - public void deactivate(ModularSyncManager manager) { - int id = inverseActiveScreens.removeInt(manager); - activeScreens.remove(id); - } + @SideOnly(Side.CLIENT) + public void closeContainer(int networkId, boolean dispose, EntityPlayerSP player) { + closeContainer(networkId, dispose, player, true); + } - public void receivePacket(PacketSyncHandler packet) { - ModularSyncManager msm = activeScreens.get(packet.networkId); - if (msm == null) return; // silently discard packets for inactive screens - try { - int id = packet.action ? 0 : packet.packet.readVarInt(); - msm.receiveWidgetUpdate(packet.panel, packet.key, packet.action, id, packet.packet); - } catch (IndexOutOfBoundsException e) { - ModularUI.LOGGER.error("Failed to read packet for sync handler {} in panel {}", packet.key, packet.panel); - } catch (IOException e) { - ModularUI.LOGGER.throwing(e); + @SideOnly(Side.CLIENT) + public void closeAll() { + closeAll(Minecraft.getMinecraft().player); } } - public void sendSyncHandlerPacket(String panel, SyncHandler syncHandler, PacketBuffer buffer, EntityPlayer player) { - ModularSyncManager msm = syncHandler.getSyncManager().getModularSyncManager(); - if (!inverseActiveScreens.containsKey(msm)) return; - int id = inverseActiveScreens.getInt(msm); - sendPacket(new PacketSyncHandler(id, panel, syncHandler.getKey(), false, buffer), player); - } + public static final class Server extends ModularNetworkSide { + + private int nextId = -1; + + private Server() { + super(false); + } + + public int activate(ModularSyncManager msm) { + if (++nextId > 100_000) nextId = 0; + activateInternal(nextId, msm); + return nextId; + } - public void sendActionPacket(ModularSyncManager msm, String panel, String key, PacketBuffer buffer, EntityPlayer player) { - if (!inverseActiveScreens.containsKey(msm)) return; - int id = inverseActiveScreens.getInt(msm); - sendPacket(new PacketSyncHandler(id, panel, key, true, buffer), player); + @Override + protected void sendPacket(IPacket packet, EntityPlayer player) { + NetworkHandler.sendToPlayer(packet, (EntityPlayerMP) player); + } + + @Override + void closeContainer(EntityPlayer player) { + ((EntityPlayerMP) player).closeContainer(); + } + + public void closeContainer(int networkId, boolean dispose, EntityPlayerMP player) { + closeContainer(networkId, dispose, player, true); + } } } diff --git a/src/main/java/com/cleanroommc/modularui/network/ModularNetworkSide.java b/src/main/java/com/cleanroommc/modularui/network/ModularNetworkSide.java new file mode 100644 index 000000000..7eb8a8735 --- /dev/null +++ b/src/main/java/com/cleanroommc/modularui/network/ModularNetworkSide.java @@ -0,0 +1,118 @@ +package com.cleanroommc.modularui.network; + +import com.cleanroommc.modularui.ModularUI; +import com.cleanroommc.modularui.network.packets.CloseAllGuiPacket; +import com.cleanroommc.modularui.network.packets.CloseGuiPacket; +import com.cleanroommc.modularui.network.packets.PacketSyncHandler; +import com.cleanroommc.modularui.screen.ModularContainer; +import com.cleanroommc.modularui.value.sync.ModularSyncManager; +import com.cleanroommc.modularui.value.sync.SyncHandler; + +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.network.PacketBuffer; + +import it.unimi.dsi.fastutil.ints.Int2ReferenceOpenHashMap; +import it.unimi.dsi.fastutil.objects.Reference2IntOpenHashMap; +import org.jetbrains.annotations.ApiStatus; + +import java.io.IOException; + +public abstract class ModularNetworkSide { + + private final boolean client; + private final Int2ReferenceOpenHashMap activeScreens = new Int2ReferenceOpenHashMap<>(); + private final Reference2IntOpenHashMap inverseActiveScreens = new Reference2IntOpenHashMap<>(); + // TODO: contextual syncer stack: in game containers shouldn't be closed in closeAll + + ModularNetworkSide(boolean client) { + this.client = client; + } + + public boolean isClient() { + return client; + } + + abstract void sendPacket(IPacket packet, EntityPlayer player); + + void activateInternal(int networkId, ModularSyncManager manager) { + if (activeScreens.containsKey(networkId)) throw new IllegalStateException("Network ID " + networkId + " is already active."); + activeScreens.put(networkId, manager); + inverseActiveScreens.put(manager, networkId); + } + + public void closeAll(EntityPlayer player) { + closeAll(player, true); + } + + @ApiStatus.Internal + public void closeAll(EntityPlayer player, boolean sync) { + if (activeScreens.isEmpty()) return; + int currentContainer = -1; + if (player.openContainer instanceof ModularContainer mc && !mc.isClientOnly()) { + currentContainer = inverseActiveScreens.getInt(mc.getSyncManager()); + } + var it = activeScreens.int2ReferenceEntrySet().fastIterator(); + while (it.hasNext()) { + var entry = it.next(); + int nid = entry.getIntKey(); + ModularSyncManager msm = entry.getValue(); + if (nid == currentContainer) closeContainer(player); + if (!msm.isClosed()) msm.onClose(); + msm.dispose(); + } + activeScreens.clear(); + inverseActiveScreens.clear(); + if (sync) sendPacket(new CloseAllGuiPacket(), player); + } + + @ApiStatus.Internal + public void receivePacket(PacketSyncHandler packet) { + ModularSyncManager msm = activeScreens.get(packet.networkId); + if (msm == null) return; // silently discard packets for inactive screens + try { + int id = packet.action ? 0 : packet.packet.readVarInt(); + msm.receiveWidgetUpdate(packet.panel, packet.key, packet.action, id, packet.packet); + } catch (IndexOutOfBoundsException e) { + ModularUI.LOGGER.error("Failed to read packet for sync handler {} in panel {}", packet.key, packet.panel); + } catch (IOException e) { + ModularUI.LOGGER.throwing(e); + } + } + + @ApiStatus.Internal + public void sendSyncHandlerPacket(String panel, SyncHandler syncHandler, PacketBuffer buffer, EntityPlayer player) { + ModularSyncManager msm = syncHandler.getSyncManager().getModularSyncManager(); + if (!inverseActiveScreens.containsKey(msm)) return; + int id = inverseActiveScreens.getInt(msm); + sendPacket(new PacketSyncHandler(id, panel, syncHandler.getKey(), false, buffer), player); + } + + @ApiStatus.Internal + public void sendActionPacket(ModularSyncManager msm, String panel, String key, PacketBuffer buffer, EntityPlayer player) { + if (!inverseActiveScreens.containsKey(msm)) return; + int id = inverseActiveScreens.getInt(msm); + sendPacket(new PacketSyncHandler(id, panel, key, true, buffer), player); + } + + @ApiStatus.Internal + public void closeContainer(int networkId, boolean dispose, EntityPlayer player, boolean sync) { + closeContainer(player); + deactivate(networkId, dispose); + if (sync) { + sendPacket(new CloseGuiPacket(networkId, dispose), player); + } + } + + abstract void closeContainer(EntityPlayer player); + + void deactivate(int networkId, boolean dispose) { + ModularSyncManager msm = activeScreens.get(networkId); + if (msm == null) return; + if (!msm.isClosed()) msm.onClose(); + if (dispose) { + activeScreens.remove(networkId); + inverseActiveScreens.removeInt(msm); + msm.dispose(); + } + } +} diff --git a/src/main/java/com/cleanroommc/modularui/network/NetworkHandler.java b/src/main/java/com/cleanroommc/modularui/network/NetworkHandler.java index 8b680ce90..ac23a5e9e 100644 --- a/src/main/java/com/cleanroommc/modularui/network/NetworkHandler.java +++ b/src/main/java/com/cleanroommc/modularui/network/NetworkHandler.java @@ -1,6 +1,8 @@ package com.cleanroommc.modularui.network; import com.cleanroommc.modularui.ModularUI; +import com.cleanroommc.modularui.network.packets.CloseAllGuiPacket; +import com.cleanroommc.modularui.network.packets.CloseGuiPacket; import com.cleanroommc.modularui.network.packets.OpenGuiPacket; import com.cleanroommc.modularui.network.packets.PacketSyncHandler; import com.cleanroommc.modularui.network.packets.SClipboard; @@ -22,11 +24,13 @@ public class NetworkHandler { public static void init() { registerS2C(SClipboard.class); - registerS2C(PacketSyncHandler.class); - registerC2S(PacketSyncHandler.class); + registerC2S(SyncConfig.class); - registerS2C(OpenGuiPacket.class); - registerC2S(OpenGuiPacket.class); + + registerBoth(OpenGuiPacket.class); + registerBoth(CloseGuiPacket.class); + registerBoth(CloseAllGuiPacket.class); + registerBoth(PacketSyncHandler.class); } private static void registerC2S(Class clazz) { @@ -37,6 +41,11 @@ private static void registerS2C(Class clazz) { CHANNEL.registerMessage(S2CHandler, clazz, packetId++, Side.CLIENT); } + private static void registerBoth(Class clazz) { + registerS2C(clazz); + registerC2S(clazz); + } + public static void sendToServer(IPacket packet) { CHANNEL.sendToServer(packet); } diff --git a/src/main/java/com/cleanroommc/modularui/network/packets/CloseAllGuiPacket.java b/src/main/java/com/cleanroommc/modularui/network/packets/CloseAllGuiPacket.java new file mode 100644 index 000000000..5af158d2b --- /dev/null +++ b/src/main/java/com/cleanroommc/modularui/network/packets/CloseAllGuiPacket.java @@ -0,0 +1,39 @@ +package com.cleanroommc.modularui.network.packets; + +import com.cleanroommc.modularui.network.IPacket; +import com.cleanroommc.modularui.network.ModularNetwork; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.network.NetHandlerPlayClient; +import net.minecraft.network.NetHandlerPlayServer; +import net.minecraft.network.PacketBuffer; +import net.minecraftforge.fml.relauncher.Side; +import net.minecraftforge.fml.relauncher.SideOnly; + +import org.jetbrains.annotations.Nullable; + +import java.io.IOException; + +public class CloseAllGuiPacket implements IPacket { + + public CloseAllGuiPacket() {} + + @Override + public void write(PacketBuffer buf) throws IOException {} + + @Override + public void read(PacketBuffer buf) throws IOException {} + + @SideOnly(Side.CLIENT) + @Override + public @Nullable IPacket executeClient(NetHandlerPlayClient handler) { + ModularNetwork.CLIENT.closeAll(Minecraft.getMinecraft().player, false); + return null; + } + + @Override + public @Nullable IPacket executeServer(NetHandlerPlayServer handler) { + ModularNetwork.SERVER.closeAll(handler.player, false); + return null; + } +} diff --git a/src/main/java/com/cleanroommc/modularui/network/packets/CloseGuiPacket.java b/src/main/java/com/cleanroommc/modularui/network/packets/CloseGuiPacket.java new file mode 100644 index 000000000..89084773e --- /dev/null +++ b/src/main/java/com/cleanroommc/modularui/network/packets/CloseGuiPacket.java @@ -0,0 +1,53 @@ +package com.cleanroommc.modularui.network.packets; + +import com.cleanroommc.modularui.network.IPacket; +import com.cleanroommc.modularui.network.ModularNetwork; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.network.NetHandlerPlayClient; +import net.minecraft.network.NetHandlerPlayServer; +import net.minecraft.network.PacketBuffer; +import net.minecraftforge.fml.relauncher.Side; +import net.minecraftforge.fml.relauncher.SideOnly; + +import org.jetbrains.annotations.Nullable; + +import java.io.IOException; + +public class CloseGuiPacket implements IPacket { + + private int networkId; + private boolean dispose; + + public CloseGuiPacket() {} + + public CloseGuiPacket(int networkId, boolean dispose) { + this.networkId = networkId; + this.dispose = dispose; + } + + @Override + public void write(PacketBuffer buf) throws IOException { + buf.writeVarInt(this.networkId); + buf.writeBoolean(this.dispose); + } + + @Override + public void read(PacketBuffer buf) throws IOException { + this.networkId = buf.readVarInt(); + this.dispose = buf.readBoolean(); + } + + @SideOnly(Side.CLIENT) + @Override + public @Nullable IPacket executeClient(NetHandlerPlayClient handler) { + ModularNetwork.CLIENT.closeContainer(this.networkId, this.dispose, Minecraft.getMinecraft().player, false); + return null; + } + + @Override + public @Nullable IPacket executeServer(NetHandlerPlayServer handler) { + ModularNetwork.SERVER.closeContainer(this.networkId, this.dispose, handler.player, false); + return null; + } +} diff --git a/src/main/java/com/cleanroommc/modularui/network/packets/OpenGuiPacket.java b/src/main/java/com/cleanroommc/modularui/network/packets/OpenGuiPacket.java index 96e63f1e8..7f7fe8ab4 100644 --- a/src/main/java/com/cleanroommc/modularui/network/packets/OpenGuiPacket.java +++ b/src/main/java/com/cleanroommc/modularui/network/packets/OpenGuiPacket.java @@ -19,13 +19,15 @@ public class OpenGuiPacket implements IPacket { + private int windowId; private int networkId; private UIFactory factory; private PacketBuffer data; public OpenGuiPacket() {} - public OpenGuiPacket(int networkId, UIFactory factory, PacketBuffer data) { + public OpenGuiPacket(int windowId, int networkId, UIFactory factory, PacketBuffer data) { + this.windowId = windowId; this.networkId = networkId; this.factory = factory; this.data = data; @@ -33,6 +35,7 @@ public OpenGuiPacket(int networkId, UIFactory factory, PacketBuffer data) { @Override public void write(PacketBuffer buf) throws IOException { + buf.writeVarIntToBuffer(this.windowId); buf.writeVarIntToBuffer(this.networkId); NetworkUtils.writeStringSafe(buf, this.factory.getFactoryName()); NetworkUtils.writeByteBuf(buf, this.data); @@ -40,6 +43,7 @@ public void write(PacketBuffer buf) throws IOException { @Override public void read(PacketBuffer buf) { + this.windowId = buf.readVarIntFromBuffer(); this.networkId = buf.readVarIntFromBuffer(); this.factory = (UIFactory) GuiManager.getFactory(NetworkUtils.readStringSafe(buf)); this.data = NetworkUtils.readPacketBuffer(buf); @@ -48,7 +52,7 @@ public void read(PacketBuffer buf) { @SideOnly(Side.CLIENT) @Override public @Nullable IPacket executeClient(NetHandlerPlayClient handler) { - GuiManager.openFromClient(this.networkId, this.factory, this.data, Platform.getClientPlayer()); + GuiManager.openFromClient(this.windowId, this.networkId, this.factory, this.data, Platform.getClientPlayer()); return null; } diff --git a/src/main/java/com/cleanroommc/modularui/network/packets/PacketSyncHandler.java b/src/main/java/com/cleanroommc/modularui/network/packets/PacketSyncHandler.java index 1b2605036..161f5cb99 100644 --- a/src/main/java/com/cleanroommc/modularui/network/packets/PacketSyncHandler.java +++ b/src/main/java/com/cleanroommc/modularui/network/packets/PacketSyncHandler.java @@ -10,6 +10,8 @@ import net.minecraft.client.network.NetHandlerPlayClient; import net.minecraft.network.NetHandlerPlayServer; import net.minecraft.network.PacketBuffer; +import net.minecraftforge.fml.relauncher.Side; +import net.minecraftforge.fml.relauncher.SideOnly; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.Nullable; @@ -51,6 +53,7 @@ public void read(PacketBuffer buf) { this.packet = NetworkUtils.readPacketBuffer(buf); } + @SideOnly(Side.CLIENT) @Override public @Nullable IPacket executeClient(NetHandlerPlayClient handler) { ModularNetwork.CLIENT.receivePacket(this); diff --git a/src/main/java/com/cleanroommc/modularui/network/packets/SClipboard.java b/src/main/java/com/cleanroommc/modularui/network/packets/SClipboard.java index 35aa9ae0d..5fc72ba64 100644 --- a/src/main/java/com/cleanroommc/modularui/network/packets/SClipboard.java +++ b/src/main/java/com/cleanroommc/modularui/network/packets/SClipboard.java @@ -9,7 +9,9 @@ import net.minecraft.entity.player.EntityPlayer; import net.minecraft.entity.player.EntityPlayerMP; import net.minecraft.network.PacketBuffer; -import cpw.mods.fml.common.FMLCommonHandler; +import net.minecraftforge.fml.common.FMLCommonHandler; +import net.minecraftforge.fml.relauncher.Side; +import net.minecraftforge.fml.relauncher.SideOnly; public class SClipboard implements IPacket { @@ -40,6 +42,7 @@ public void read(PacketBuffer buf) { this.s = NetworkUtils.readStringSafe(buf); } + @SideOnly(Side.CLIENT) @Override public IPacket executeClient(NetHandlerPlayClient handler) { GuiScreen.setClipboardString(this.s); diff --git a/src/main/java/com/cleanroommc/modularui/screen/ClientScreenHandler.java b/src/main/java/com/cleanroommc/modularui/screen/ClientScreenHandler.java index 96bdbf347..14b79c03b 100644 --- a/src/main/java/com/cleanroommc/modularui/screen/ClientScreenHandler.java +++ b/src/main/java/com/cleanroommc/modularui/screen/ClientScreenHandler.java @@ -17,6 +17,7 @@ import com.cleanroommc.modularui.core.mixins.early.minecraft.GuiScreenAccessor; import com.cleanroommc.modularui.drawable.GuiDraw; import com.cleanroommc.modularui.drawable.Stencil; +import com.cleanroommc.modularui.network.ModularNetwork; import com.cleanroommc.modularui.overlay.OverlayManager; import com.cleanroommc.modularui.overlay.OverlayStack; import com.cleanroommc.modularui.screen.viewport.GuiContext; @@ -231,6 +232,8 @@ private static void onGuiChanged(GuiScreen oldScreen, GuiScreen newScreen) { } else if (newScreen == null) { // closing -> clear stack and dispose every screen invalidateMuiStack(); + // only when all screens are closed dispose all containers in the stack + ModularNetwork.CLIENT.closeAll(); } OverlayManager.onGuiOpen(newScreen); diff --git a/src/main/java/com/cleanroommc/modularui/screen/ModularContainer.java b/src/main/java/com/cleanroommc/modularui/screen/ModularContainer.java index 47bdfd84a..62b8350ef 100644 --- a/src/main/java/com/cleanroommc/modularui/screen/ModularContainer.java +++ b/src/main/java/com/cleanroommc/modularui/screen/ModularContainer.java @@ -4,7 +4,6 @@ import com.cleanroommc.modularui.api.inventory.ClickType; import com.cleanroommc.modularui.core.mixins.early.minecraft.ContainerAccessor; import com.cleanroommc.modularui.factory.GuiData; -import com.cleanroommc.modularui.network.ModularNetwork; import com.cleanroommc.modularui.network.NetworkUtils; import com.cleanroommc.modularui.utils.Platform; import com.cleanroommc.modularui.utils.item.ItemHandlerHelper; @@ -98,25 +97,16 @@ public ContainerAccessor acc() { return (ContainerAccessor) this; } - @MustBeInvokedByOverriders - public void onModularContainerOpened() { - if (this.syncManager != null) { - this.syncManager.onOpen(); - } - } + public void onModularContainerOpened() {} /** * Called when this container closes. This is different to {@link Container#onContainerClosed(EntityPlayer)}, since that one is also * called from {@link GuiContainer#onGuiClosed()}, which means it is called even when the container may still exist. * This happens when a temporary client screen takes over (like JEI,NEI,etc.). This is only called when the container actually closes. */ - @MustBeInvokedByOverriders - public void onModularContainerClosed() { - if (this.syncManager != null) { - this.syncManager.dispose(); - ModularNetwork.get(this.player).deactivate(this.syncManager); - } - } + public void onModularContainerClosed() {} + + public void onModularContainerDisposed() {} @MustBeInvokedByOverriders @Override diff --git a/src/main/java/com/cleanroommc/modularui/value/sync/ModularSyncManager.java b/src/main/java/com/cleanroommc/modularui/value/sync/ModularSyncManager.java index b1ecd081e..f68efa1e7 100644 --- a/src/main/java/com/cleanroommc/modularui/value/sync/ModularSyncManager.java +++ b/src/main/java/com/cleanroommc/modularui/value/sync/ModularSyncManager.java @@ -41,6 +41,7 @@ public class ModularSyncManager implements ISyncRegistrar { private ModularContainer container; private final CursorSlotSyncHandler cursorSlotSyncHandler = new CursorSlotSyncHandler(); private final boolean client; + private State state = State.INIT; public ModularSyncManager(boolean client) { this.client = client; @@ -68,17 +69,38 @@ public boolean isClient() { return this.client; } + @ApiStatus.Internal public void detectAndSendChanges(boolean init) { this.panelSyncManagerMap.values().forEach(psm -> psm.detectAndSendChanges(init)); } + @ApiStatus.Internal public void dispose() { + if (isDisposed()) return; + if (!isClosed()) throw new IllegalStateException("Sync manager must be closed before disposing!"); this.panelSyncManagerMap.values().forEach(PanelSyncManager::onClose); this.panelSyncManagerMap.clear(); + this.container.onModularContainerDisposed(); + setState(State.DISPOSED); } + @ApiStatus.Internal public void onOpen() { + if (isOpen()) return; + if (isDisposed()) throw new IllegalStateException("Can't open sync manager after it has been disposed!"); + if (this.container == null) { + throw new IllegalStateException("Sync Manager can't be opened when its not yet constructed. ModularContainer is null."); + } + setState(State.OPEN); this.panelSyncManagerMap.values().forEach(PanelSyncManager::onOpen); + this.container.onModularContainerOpened(); + } + + @ApiStatus.Internal + public void onClose() { + if (!isOpen()) throw new IllegalStateException(); + this.container.onModularContainerClosed(); + setState(State.CLOSED); } public void onUpdate() { @@ -108,12 +130,14 @@ public void setCursorItem(ItemStack item) { this.cursorSlotSyncHandler.sync(); } + @ApiStatus.Internal public void open(String name, PanelSyncManager syncManager) { this.panelSyncManagerMap.put(name, syncManager); this.panelHistory.add(name); syncManager.initialize(name); } + @ApiStatus.Internal public void close(String name) { PanelSyncManager psm = this.panelSyncManagerMap.remove(name); if (psm != null) psm.onClose(); @@ -123,6 +147,7 @@ public boolean isOpen(String panelName) { return this.panelSyncManagerMap.containsKey(panelName); } + @ApiStatus.Internal public void receiveWidgetUpdate(String panelName, String mapKey, boolean action, int id, PacketBuffer buf) throws IOException { PanelSyncManager psm = this.panelSyncManagerMap.get(panelName); if (psm != null) { @@ -209,4 +234,24 @@ public SlotGroup getSlotGroup(String name) { public static String makeSyncKey(String name, int id) { return ISyncRegistrar.makeSyncKey(name, id); } + + public boolean isOpen() { + return this.state == State.OPEN; + } + + public boolean isClosed() { + return this.state == State.CLOSED || this.state == State.DISPOSED; + } + + public boolean isDisposed() { + return this.state == State.DISPOSED; + } + + private void setState(State state) { + this.state = state; + } + + enum State { + INIT, OPEN, CLOSED, DISPOSED; + } } diff --git a/src/main/java/com/cleanroommc/modularui/value/sync/SyncHandler.java b/src/main/java/com/cleanroommc/modularui/value/sync/SyncHandler.java index 47f2355f9..943b725e3 100644 --- a/src/main/java/com/cleanroommc/modularui/value/sync/SyncHandler.java +++ b/src/main/java/com/cleanroommc/modularui/value/sync/SyncHandler.java @@ -3,6 +3,7 @@ import com.cleanroommc.modularui.api.IPacketWriter; import com.cleanroommc.modularui.api.value.ISyncOrValue; import com.cleanroommc.modularui.network.ModularNetwork; +import com.cleanroommc.modularui.network.ModularNetworkSide; import net.minecraft.network.PacketBuffer; import cpw.mods.fml.relauncher.Side; @@ -181,7 +182,7 @@ public boolean isSyncHandler() { return true; } - private static void send(ModularNetwork network, String panel, PacketBuffer buffer, SyncHandler syncHandler) { + private static void send(ModularNetworkSide network, String panel, PacketBuffer buffer, SyncHandler syncHandler) { Objects.requireNonNull(buffer); Objects.requireNonNull(syncHandler); if (!syncHandler.isValid()) { From 5d2c9dce771759633c41e7142538c7df31591a5f Mon Sep 17 00:00:00 2001 From: brachy84 Date: Sat, 3 Jan 2026 13:25:47 +0100 Subject: [PATCH 45/50] ability to reopen containers (cherry picked from commit 043d673197c5f345b116a973d716ccc9b0219734) --- .../cleanroommc/modularui/api/MCHelper.java | 5 ++ .../modularui/factory/GuiManager.java | 2 +- .../modularui/network/ModularNetwork.java | 10 ++++ .../modularui/network/ModularNetworkSide.java | 19 ++++++++ .../modularui/network/NetworkHandler.java | 2 + .../network/packets/ReopenGuiPacket.java | 46 +++++++++++++++++++ 6 files changed, 83 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/cleanroommc/modularui/network/packets/ReopenGuiPacket.java diff --git a/src/main/java/com/cleanroommc/modularui/api/MCHelper.java b/src/main/java/com/cleanroommc/modularui/api/MCHelper.java index 43ead8323..937af0b9e 100644 --- a/src/main/java/com/cleanroommc/modularui/api/MCHelper.java +++ b/src/main/java/com/cleanroommc/modularui/api/MCHelper.java @@ -2,6 +2,10 @@ import com.cleanroommc.modularui.ModularUI; import com.cleanroommc.modularui.api.widget.Interactable; +import com.cleanroommc.modularui.ModularUIConfig; +import com.cleanroommc.modularui.api.drawable.IKey; +import com.cleanroommc.modularui.network.ModularNetwork; +import com.cleanroommc.modularui.network.NetworkUtils; import net.minecraft.client.Minecraft; import net.minecraft.client.entity.EntityClientPlayerMP; @@ -63,6 +67,7 @@ public static void popScreen(boolean openParentOnClose, GuiScreen parent) { // instead they are kept in a stack until all screens are closed if (openParentOnClose) { Minecraft.getMinecraft().displayGuiScreen(parent); + ModularNetwork.CLIENT.reopenSyncerOf(parent); } else { Minecraft.getMinecraft().displayGuiScreen(null); } diff --git a/src/main/java/com/cleanroommc/modularui/factory/GuiManager.java b/src/main/java/com/cleanroommc/modularui/factory/GuiManager.java index 57555c864..81cc29eb7 100644 --- a/src/main/java/com/cleanroommc/modularui/factory/GuiManager.java +++ b/src/main/java/com/cleanroommc/modularui/factory/GuiManager.java @@ -88,7 +88,7 @@ public static void open(@NotNull UIFactory factory, @NotN factory.writeGuiData(guiData, buffer); int nid = ModularNetwork.SERVER.activate(msm); NetworkHandler.sendToPlayer(new OpenGuiPacket<>(windowId, nid, factory, buffer), player); - // open container // this mimics forge behaviour + // open container // this mimics forge behavior player.openContainer = container; player.openContainer.windowId = windowId; player.openContainer.addCraftingToCrafters(player); diff --git a/src/main/java/com/cleanroommc/modularui/network/ModularNetwork.java b/src/main/java/com/cleanroommc/modularui/network/ModularNetwork.java index ca9ec46d5..3c8444f1c 100644 --- a/src/main/java/com/cleanroommc/modularui/network/ModularNetwork.java +++ b/src/main/java/com/cleanroommc/modularui/network/ModularNetwork.java @@ -1,9 +1,11 @@ package com.cleanroommc.modularui.network; +import com.cleanroommc.modularui.api.IMuiScreen; import com.cleanroommc.modularui.value.sync.ModularSyncManager; import net.minecraft.client.Minecraft; import net.minecraft.client.entity.EntityPlayerSP; +import net.minecraft.client.gui.GuiScreen; import net.minecraft.entity.player.EntityPlayer; import net.minecraft.entity.player.EntityPlayerMP; import net.minecraft.item.ItemStack; @@ -64,6 +66,14 @@ public void closeContainer(int networkId, boolean dispose, EntityPlayerSP player public void closeAll() { closeAll(Minecraft.getMinecraft().player); } + + @SideOnly(Side.CLIENT) + public void reopenSyncerOf(GuiScreen guiScreen) { + if (guiScreen instanceof IMuiScreen ms && !ms.getScreen().isClientOnly()) { + ModularSyncManager msm = ms.getScreen().getSyncManager(); + reopen(Minecraft.getMinecraft().player, msm, true); + } + } } public static final class Server extends ModularNetworkSide { diff --git a/src/main/java/com/cleanroommc/modularui/network/ModularNetworkSide.java b/src/main/java/com/cleanroommc/modularui/network/ModularNetworkSide.java index 7eb8a8735..658dafbae 100644 --- a/src/main/java/com/cleanroommc/modularui/network/ModularNetworkSide.java +++ b/src/main/java/com/cleanroommc/modularui/network/ModularNetworkSide.java @@ -4,12 +4,15 @@ import com.cleanroommc.modularui.network.packets.CloseAllGuiPacket; import com.cleanroommc.modularui.network.packets.CloseGuiPacket; import com.cleanroommc.modularui.network.packets.PacketSyncHandler; +import com.cleanroommc.modularui.network.packets.ReopenGuiPacket; import com.cleanroommc.modularui.screen.ModularContainer; import com.cleanroommc.modularui.value.sync.ModularSyncManager; import com.cleanroommc.modularui.value.sync.SyncHandler; import net.minecraft.entity.player.EntityPlayer; import net.minecraft.network.PacketBuffer; +import net.minecraftforge.common.MinecraftForge; +import net.minecraftforge.event.entity.player.PlayerContainerEvent; import it.unimi.dsi.fastutil.ints.Int2ReferenceOpenHashMap; import it.unimi.dsi.fastutil.objects.Reference2IntOpenHashMap; @@ -115,4 +118,20 @@ void deactivate(int networkId, boolean dispose) { msm.dispose(); } } + + @ApiStatus.Internal + public void reopen(EntityPlayer player, ModularSyncManager msm, boolean sync) { + if (player.openContainer != msm.getContainer()) { + closeContainer(player); + player.openContainer = msm.getContainer(); + msm.onOpen(); + MinecraftForge.EVENT_BUS.post(new PlayerContainerEvent.Open(player, msm.getContainer())); + } + if (sync) sendPacket(new ReopenGuiPacket(inverseActiveScreens.getInt(msm)), player); + } + + @ApiStatus.Internal + public void reopen(EntityPlayer player, int networkId, boolean sync) { + reopen(player, activeScreens.get(networkId), sync); + } } diff --git a/src/main/java/com/cleanroommc/modularui/network/NetworkHandler.java b/src/main/java/com/cleanroommc/modularui/network/NetworkHandler.java index ac23a5e9e..753d49302 100644 --- a/src/main/java/com/cleanroommc/modularui/network/NetworkHandler.java +++ b/src/main/java/com/cleanroommc/modularui/network/NetworkHandler.java @@ -5,6 +5,7 @@ import com.cleanroommc.modularui.network.packets.CloseGuiPacket; import com.cleanroommc.modularui.network.packets.OpenGuiPacket; import com.cleanroommc.modularui.network.packets.PacketSyncHandler; +import com.cleanroommc.modularui.network.packets.ReopenGuiPacket; import com.cleanroommc.modularui.network.packets.SClipboard; import com.cleanroommc.modularui.network.packets.SyncConfig; @@ -28,6 +29,7 @@ public static void init() { registerC2S(SyncConfig.class); registerBoth(OpenGuiPacket.class); + registerBoth(ReopenGuiPacket.class); registerBoth(CloseGuiPacket.class); registerBoth(CloseAllGuiPacket.class); registerBoth(PacketSyncHandler.class); diff --git a/src/main/java/com/cleanroommc/modularui/network/packets/ReopenGuiPacket.java b/src/main/java/com/cleanroommc/modularui/network/packets/ReopenGuiPacket.java new file mode 100644 index 000000000..905854f2d --- /dev/null +++ b/src/main/java/com/cleanroommc/modularui/network/packets/ReopenGuiPacket.java @@ -0,0 +1,46 @@ +package com.cleanroommc.modularui.network.packets; + +import com.cleanroommc.modularui.network.IPacket; +import com.cleanroommc.modularui.network.ModularNetwork; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.network.NetHandlerPlayClient; +import net.minecraft.network.NetHandlerPlayServer; +import net.minecraft.network.PacketBuffer; + +import org.jetbrains.annotations.Nullable; + +import java.io.IOException; + +public class ReopenGuiPacket implements IPacket { + + private int networkId; + + public ReopenGuiPacket() {} + + public ReopenGuiPacket(int networkId) { + this.networkId = networkId; + } + + @Override + public void write(PacketBuffer buf) throws IOException { + buf.writeInt(networkId); + } + + @Override + public void read(PacketBuffer buf) throws IOException { + this.networkId = buf.readInt(); + } + + @Override + public @Nullable IPacket executeClient(NetHandlerPlayClient handler) { + ModularNetwork.CLIENT.reopen(Minecraft.getMinecraft().player, this.networkId, false); + return null; + } + + @Override + public @Nullable IPacket executeServer(NetHandlerPlayServer handler) { + ModularNetwork.SERVER.reopen(handler.player, this.networkId, false); + return null; + } +} From 94d4abad5ed58a84f250a6e1dfb34758ebd21aee Mon Sep 17 00:00:00 2001 From: brachy <45517902+brachy84@users.noreply.github.com> Date: Sun, 4 Jan 2026 11:27:23 +0100 Subject: [PATCH 46/50] Begone mXparser (#188) * replace mxparser * . * fix postfix op overwriting infix op * use X instead of E for exa si prefix to avoid variable clashing * some big decimal things * fix (cherry picked from commit 51f2dbb9b8685b6016b7e20c188916eee444d699) --- dependencies.gradle | 8 +- .../com/cleanroommc/modularui/ModularUI.java | 8 +- .../modularui/api/drawable/IDrawable.java | 1 - .../modularui/utils/MathUtils.java | 51 +- .../modularui/utils/NumberFormat.java | 29 + .../modularui/utils/ParseResult.java | 29 +- .../cleanroommc/modularui/utils/SIPrefix.java | 22 +- .../utils/math/PostfixPercentOperator.java | 29 + .../widgets/textfield/TextFieldWidget.java | 6 +- .../com/ezylang/evalex/BaseException.java | 42 ++ .../ezylang/evalex/EvaluationException.java | 36 + .../java/com/ezylang/evalex/Expression.java | 469 +++++++++++++ src/main/java/com/ezylang/evalex/LICENSE | 201 ++++++ src/main/java/com/ezylang/evalex/README.md | 298 ++++++++ .../config/ExpressionConfiguration.java | 514 ++++++++++++++ .../evalex/config/FunctionDictionaryIfc.java | 76 ++ .../config/MapBasedFunctionDictionary.java | 69 ++ .../config/MapBasedOperatorDictionary.java | 109 +++ .../evalex/config/OperatorDictionaryIfc.java | 156 +++++ .../ezylang/evalex/data/DataAccessorIfc.java | 40 ++ .../ezylang/evalex/data/EvaluationValue.java | 576 ++++++++++++++++ .../evalex/data/MapBasedDataAccessor.java | 39 ++ .../data/conversion/ArrayConverter.java | 159 +++++ .../data/conversion/BinaryConverter.java | 38 + .../data/conversion/BooleanConverter.java | 35 + .../evalex/data/conversion/ConverterIfc.java | 47 ++ .../data/conversion/DateTimeConverter.java | 111 +++ .../DefaultEvaluationValueConverter.java | 94 +++ .../data/conversion/DurationConverter.java | 37 + .../EvaluationValueConverterIfc.java | 36 + .../conversion/ExpressionNodeConverter.java | 36 + .../data/conversion/NumberConverter.java | 67 ++ .../data/conversion/StringConverter.java | 45 ++ .../data/conversion/StructureConverter.java | 43 ++ .../evalex/functions/AbstractFunction.java | 103 +++ .../ezylang/evalex/functions/FunctionIfc.java | 94 +++ .../evalex/functions/FunctionParameter.java | 58 ++ .../FunctionParameterDefinition.java | 57 ++ .../evalex/functions/FunctionParameters.java | 33 + .../evalex/functions/basic/AbsFunction.java | 37 + .../basic/AbstractMinMaxFunction.java | 45 ++ .../functions/basic/AverageFunction.java | 82 +++ .../functions/basic/CeilingFunction.java | 40 ++ .../functions/basic/CoalesceFunction.java | 41 ++ .../evalex/functions/basic/FactFunction.java | 45 ++ .../evalex/functions/basic/FloorFunction.java | 40 ++ .../evalex/functions/basic/IfFunction.java | 46 ++ .../evalex/functions/basic/Log10Function.java | 38 + .../evalex/functions/basic/LogFunction.java | 38 + .../evalex/functions/basic/MaxFunction.java | 41 ++ .../evalex/functions/basic/MinFunction.java | 41 ++ .../evalex/functions/basic/NotFunction.java | 38 + .../functions/basic/RandomFunction.java | 38 + .../evalex/functions/basic/RoundFunction.java | 46 ++ .../evalex/functions/basic/SqrtFunction.java | 66 ++ .../evalex/functions/basic/SumFunction.java | 57 ++ .../functions/basic/SwitchFunction.java | 102 +++ .../datetime/DateTimeFormatFunction.java | 74 ++ .../datetime/DateTimeNewFunction.java | 124 ++++ .../datetime/DateTimeNowFunction.java | 47 ++ .../datetime/DateTimeParseFunction.java | 85 +++ .../datetime/DateTimeToEpochFunction.java | 35 + .../datetime/DateTimeTodayFunction.java | 67 ++ .../datetime/DurationFromMillisFunction.java | 39 ++ .../datetime/DurationNewFunction.java | 58 ++ .../datetime/DurationParseFunction.java | 39 ++ .../datetime/DurationToMillisFunction.java | 35 + .../functions/datetime/ZoneIdConverter.java | 53 ++ .../functions/string/StringContains.java | 42 ++ .../string/StringEndsWithFunction.java | 40 ++ .../string/StringFormatFunction.java | 87 +++ .../functions/string/StringLeftFunction.java | 64 ++ .../string/StringLengthFunction.java | 39 ++ .../functions/string/StringLowerFunction.java | 35 + .../string/StringMatchesFunction.java | 42 ++ .../functions/string/StringRightFunction.java | 64 ++ .../functions/string/StringSplitFunction.java | 53 ++ .../string/StringStartsWithFunction.java | 40 ++ .../string/StringSubstringFunction.java | 64 ++ .../functions/string/StringTrimFunction.java | 39 ++ .../functions/string/StringUpperFunction.java | 35 + .../functions/trigonometric/AcosFunction.java | 52 ++ .../trigonometric/AcosHFunction.java | 43 ++ .../trigonometric/AcosRFunction.java | 53 ++ .../functions/trigonometric/AcotFunction.java | 39 ++ .../trigonometric/AcotHFunction.java | 38 + .../trigonometric/AcotRFunction.java | 38 + .../functions/trigonometric/AsinFunction.java | 52 ++ .../trigonometric/AsinHFunction.java | 38 + .../trigonometric/AsinRFunction.java | 56 ++ .../trigonometric/Atan2Function.java | 41 ++ .../trigonometric/Atan2RFunction.java | 40 ++ .../functions/trigonometric/AtanFunction.java | 37 + .../trigonometric/AtanHFunction.java | 43 ++ .../trigonometric/AtanRFunction.java | 37 + .../functions/trigonometric/CosFunction.java | 37 + .../functions/trigonometric/CosHFunction.java | 37 + .../functions/trigonometric/CosRFunction.java | 37 + .../functions/trigonometric/CotFunction.java | 38 + .../functions/trigonometric/CotHFunction.java | 38 + .../functions/trigonometric/CotRFunction.java | 38 + .../functions/trigonometric/CscFunction.java | 38 + .../functions/trigonometric/CscHFunction.java | 38 + .../functions/trigonometric/CscRFunction.java | 38 + .../functions/trigonometric/DegFunction.java | 38 + .../functions/trigonometric/RadFunction.java | 38 + .../functions/trigonometric/SecFunction.java | 38 + .../functions/trigonometric/SecHFunction.java | 38 + .../functions/trigonometric/SecRFunction.java | 38 + .../functions/trigonometric/SinFunction.java | 37 + .../functions/trigonometric/SinHFunction.java | 37 + .../functions/trigonometric/SinRFunction.java | 37 + .../functions/trigonometric/TanFunction.java | 37 + .../functions/trigonometric/TanHFunction.java | 37 + .../functions/trigonometric/TanRFunction.java | 37 + .../evalex/operators/AbstractOperator.java | 94 +++ .../evalex/operators/InfixOperator.java | 46 ++ .../OperatorAnnotationNotFoundException.java | 27 + .../ezylang/evalex/operators/OperatorIfc.java | 157 +++++ .../evalex/operators/PostfixOperator.java | 43 ++ .../evalex/operators/PrefixOperator.java | 43 ++ .../arithmetic/InfixDivisionOperator.java | 57 ++ .../arithmetic/InfixMinusOperator.java | 70 ++ .../arithmetic/InfixModuloOperator.java | 57 ++ .../InfixMultiplicationOperator.java | 50 ++ .../arithmetic/InfixPlusOperator.java | 60 ++ .../arithmetic/InfixPowerOfOperator.java | 80 +++ .../arithmetic/PrefixMinusOperator.java | 44 ++ .../arithmetic/PrefixPlusOperator.java | 44 ++ .../operators/booleans/InfixAndOperator.java | 41 ++ .../booleans/InfixEqualsOperator.java | 43 ++ .../booleans/InfixGreaterEqualsOperator.java | 37 + .../booleans/InfixGreaterOperator.java | 37 + .../booleans/InfixLessEqualsOperator.java | 37 + .../operators/booleans/InfixLessOperator.java | 37 + .../booleans/InfixNotEqualsOperator.java | 43 ++ .../operators/booleans/InfixOrOperator.java | 41 ++ .../operators/booleans/PrefixNotOperator.java | 35 + .../com/ezylang/evalex/parser/ASTNode.java | 73 ++ .../ezylang/evalex/parser/ParseException.java | 42 ++ .../evalex/parser/ShuntingYardConverter.java | 292 ++++++++ .../java/com/ezylang/evalex/parser/Token.java | 80 +++ .../com/ezylang/evalex/parser/Tokenizer.java | 652 ++++++++++++++++++ 143 files changed, 9751 insertions(+), 51 deletions(-) create mode 100644 src/main/java/com/cleanroommc/modularui/utils/math/PostfixPercentOperator.java create mode 100644 src/main/java/com/ezylang/evalex/BaseException.java create mode 100644 src/main/java/com/ezylang/evalex/EvaluationException.java create mode 100644 src/main/java/com/ezylang/evalex/Expression.java create mode 100644 src/main/java/com/ezylang/evalex/LICENSE create mode 100644 src/main/java/com/ezylang/evalex/README.md create mode 100644 src/main/java/com/ezylang/evalex/config/ExpressionConfiguration.java create mode 100644 src/main/java/com/ezylang/evalex/config/FunctionDictionaryIfc.java create mode 100644 src/main/java/com/ezylang/evalex/config/MapBasedFunctionDictionary.java create mode 100644 src/main/java/com/ezylang/evalex/config/MapBasedOperatorDictionary.java create mode 100644 src/main/java/com/ezylang/evalex/config/OperatorDictionaryIfc.java create mode 100644 src/main/java/com/ezylang/evalex/data/DataAccessorIfc.java create mode 100644 src/main/java/com/ezylang/evalex/data/EvaluationValue.java create mode 100644 src/main/java/com/ezylang/evalex/data/MapBasedDataAccessor.java create mode 100644 src/main/java/com/ezylang/evalex/data/conversion/ArrayConverter.java create mode 100644 src/main/java/com/ezylang/evalex/data/conversion/BinaryConverter.java create mode 100644 src/main/java/com/ezylang/evalex/data/conversion/BooleanConverter.java create mode 100644 src/main/java/com/ezylang/evalex/data/conversion/ConverterIfc.java create mode 100644 src/main/java/com/ezylang/evalex/data/conversion/DateTimeConverter.java create mode 100644 src/main/java/com/ezylang/evalex/data/conversion/DefaultEvaluationValueConverter.java create mode 100644 src/main/java/com/ezylang/evalex/data/conversion/DurationConverter.java create mode 100644 src/main/java/com/ezylang/evalex/data/conversion/EvaluationValueConverterIfc.java create mode 100644 src/main/java/com/ezylang/evalex/data/conversion/ExpressionNodeConverter.java create mode 100644 src/main/java/com/ezylang/evalex/data/conversion/NumberConverter.java create mode 100644 src/main/java/com/ezylang/evalex/data/conversion/StringConverter.java create mode 100644 src/main/java/com/ezylang/evalex/data/conversion/StructureConverter.java create mode 100644 src/main/java/com/ezylang/evalex/functions/AbstractFunction.java create mode 100644 src/main/java/com/ezylang/evalex/functions/FunctionIfc.java create mode 100644 src/main/java/com/ezylang/evalex/functions/FunctionParameter.java create mode 100644 src/main/java/com/ezylang/evalex/functions/FunctionParameterDefinition.java create mode 100644 src/main/java/com/ezylang/evalex/functions/FunctionParameters.java create mode 100644 src/main/java/com/ezylang/evalex/functions/basic/AbsFunction.java create mode 100644 src/main/java/com/ezylang/evalex/functions/basic/AbstractMinMaxFunction.java create mode 100644 src/main/java/com/ezylang/evalex/functions/basic/AverageFunction.java create mode 100644 src/main/java/com/ezylang/evalex/functions/basic/CeilingFunction.java create mode 100644 src/main/java/com/ezylang/evalex/functions/basic/CoalesceFunction.java create mode 100644 src/main/java/com/ezylang/evalex/functions/basic/FactFunction.java create mode 100644 src/main/java/com/ezylang/evalex/functions/basic/FloorFunction.java create mode 100644 src/main/java/com/ezylang/evalex/functions/basic/IfFunction.java create mode 100644 src/main/java/com/ezylang/evalex/functions/basic/Log10Function.java create mode 100644 src/main/java/com/ezylang/evalex/functions/basic/LogFunction.java create mode 100644 src/main/java/com/ezylang/evalex/functions/basic/MaxFunction.java create mode 100644 src/main/java/com/ezylang/evalex/functions/basic/MinFunction.java create mode 100644 src/main/java/com/ezylang/evalex/functions/basic/NotFunction.java create mode 100644 src/main/java/com/ezylang/evalex/functions/basic/RandomFunction.java create mode 100644 src/main/java/com/ezylang/evalex/functions/basic/RoundFunction.java create mode 100644 src/main/java/com/ezylang/evalex/functions/basic/SqrtFunction.java create mode 100644 src/main/java/com/ezylang/evalex/functions/basic/SumFunction.java create mode 100644 src/main/java/com/ezylang/evalex/functions/basic/SwitchFunction.java create mode 100644 src/main/java/com/ezylang/evalex/functions/datetime/DateTimeFormatFunction.java create mode 100644 src/main/java/com/ezylang/evalex/functions/datetime/DateTimeNewFunction.java create mode 100644 src/main/java/com/ezylang/evalex/functions/datetime/DateTimeNowFunction.java create mode 100644 src/main/java/com/ezylang/evalex/functions/datetime/DateTimeParseFunction.java create mode 100644 src/main/java/com/ezylang/evalex/functions/datetime/DateTimeToEpochFunction.java create mode 100644 src/main/java/com/ezylang/evalex/functions/datetime/DateTimeTodayFunction.java create mode 100644 src/main/java/com/ezylang/evalex/functions/datetime/DurationFromMillisFunction.java create mode 100644 src/main/java/com/ezylang/evalex/functions/datetime/DurationNewFunction.java create mode 100644 src/main/java/com/ezylang/evalex/functions/datetime/DurationParseFunction.java create mode 100644 src/main/java/com/ezylang/evalex/functions/datetime/DurationToMillisFunction.java create mode 100644 src/main/java/com/ezylang/evalex/functions/datetime/ZoneIdConverter.java create mode 100644 src/main/java/com/ezylang/evalex/functions/string/StringContains.java create mode 100644 src/main/java/com/ezylang/evalex/functions/string/StringEndsWithFunction.java create mode 100644 src/main/java/com/ezylang/evalex/functions/string/StringFormatFunction.java create mode 100644 src/main/java/com/ezylang/evalex/functions/string/StringLeftFunction.java create mode 100644 src/main/java/com/ezylang/evalex/functions/string/StringLengthFunction.java create mode 100644 src/main/java/com/ezylang/evalex/functions/string/StringLowerFunction.java create mode 100644 src/main/java/com/ezylang/evalex/functions/string/StringMatchesFunction.java create mode 100644 src/main/java/com/ezylang/evalex/functions/string/StringRightFunction.java create mode 100644 src/main/java/com/ezylang/evalex/functions/string/StringSplitFunction.java create mode 100644 src/main/java/com/ezylang/evalex/functions/string/StringStartsWithFunction.java create mode 100644 src/main/java/com/ezylang/evalex/functions/string/StringSubstringFunction.java create mode 100644 src/main/java/com/ezylang/evalex/functions/string/StringTrimFunction.java create mode 100644 src/main/java/com/ezylang/evalex/functions/string/StringUpperFunction.java create mode 100644 src/main/java/com/ezylang/evalex/functions/trigonometric/AcosFunction.java create mode 100644 src/main/java/com/ezylang/evalex/functions/trigonometric/AcosHFunction.java create mode 100644 src/main/java/com/ezylang/evalex/functions/trigonometric/AcosRFunction.java create mode 100644 src/main/java/com/ezylang/evalex/functions/trigonometric/AcotFunction.java create mode 100644 src/main/java/com/ezylang/evalex/functions/trigonometric/AcotHFunction.java create mode 100644 src/main/java/com/ezylang/evalex/functions/trigonometric/AcotRFunction.java create mode 100644 src/main/java/com/ezylang/evalex/functions/trigonometric/AsinFunction.java create mode 100644 src/main/java/com/ezylang/evalex/functions/trigonometric/AsinHFunction.java create mode 100644 src/main/java/com/ezylang/evalex/functions/trigonometric/AsinRFunction.java create mode 100644 src/main/java/com/ezylang/evalex/functions/trigonometric/Atan2Function.java create mode 100644 src/main/java/com/ezylang/evalex/functions/trigonometric/Atan2RFunction.java create mode 100644 src/main/java/com/ezylang/evalex/functions/trigonometric/AtanFunction.java create mode 100644 src/main/java/com/ezylang/evalex/functions/trigonometric/AtanHFunction.java create mode 100644 src/main/java/com/ezylang/evalex/functions/trigonometric/AtanRFunction.java create mode 100644 src/main/java/com/ezylang/evalex/functions/trigonometric/CosFunction.java create mode 100644 src/main/java/com/ezylang/evalex/functions/trigonometric/CosHFunction.java create mode 100644 src/main/java/com/ezylang/evalex/functions/trigonometric/CosRFunction.java create mode 100644 src/main/java/com/ezylang/evalex/functions/trigonometric/CotFunction.java create mode 100644 src/main/java/com/ezylang/evalex/functions/trigonometric/CotHFunction.java create mode 100644 src/main/java/com/ezylang/evalex/functions/trigonometric/CotRFunction.java create mode 100644 src/main/java/com/ezylang/evalex/functions/trigonometric/CscFunction.java create mode 100644 src/main/java/com/ezylang/evalex/functions/trigonometric/CscHFunction.java create mode 100644 src/main/java/com/ezylang/evalex/functions/trigonometric/CscRFunction.java create mode 100644 src/main/java/com/ezylang/evalex/functions/trigonometric/DegFunction.java create mode 100644 src/main/java/com/ezylang/evalex/functions/trigonometric/RadFunction.java create mode 100644 src/main/java/com/ezylang/evalex/functions/trigonometric/SecFunction.java create mode 100644 src/main/java/com/ezylang/evalex/functions/trigonometric/SecHFunction.java create mode 100644 src/main/java/com/ezylang/evalex/functions/trigonometric/SecRFunction.java create mode 100644 src/main/java/com/ezylang/evalex/functions/trigonometric/SinFunction.java create mode 100644 src/main/java/com/ezylang/evalex/functions/trigonometric/SinHFunction.java create mode 100644 src/main/java/com/ezylang/evalex/functions/trigonometric/SinRFunction.java create mode 100644 src/main/java/com/ezylang/evalex/functions/trigonometric/TanFunction.java create mode 100644 src/main/java/com/ezylang/evalex/functions/trigonometric/TanHFunction.java create mode 100644 src/main/java/com/ezylang/evalex/functions/trigonometric/TanRFunction.java create mode 100644 src/main/java/com/ezylang/evalex/operators/AbstractOperator.java create mode 100644 src/main/java/com/ezylang/evalex/operators/InfixOperator.java create mode 100644 src/main/java/com/ezylang/evalex/operators/OperatorAnnotationNotFoundException.java create mode 100644 src/main/java/com/ezylang/evalex/operators/OperatorIfc.java create mode 100644 src/main/java/com/ezylang/evalex/operators/PostfixOperator.java create mode 100644 src/main/java/com/ezylang/evalex/operators/PrefixOperator.java create mode 100644 src/main/java/com/ezylang/evalex/operators/arithmetic/InfixDivisionOperator.java create mode 100644 src/main/java/com/ezylang/evalex/operators/arithmetic/InfixMinusOperator.java create mode 100644 src/main/java/com/ezylang/evalex/operators/arithmetic/InfixModuloOperator.java create mode 100644 src/main/java/com/ezylang/evalex/operators/arithmetic/InfixMultiplicationOperator.java create mode 100644 src/main/java/com/ezylang/evalex/operators/arithmetic/InfixPlusOperator.java create mode 100644 src/main/java/com/ezylang/evalex/operators/arithmetic/InfixPowerOfOperator.java create mode 100644 src/main/java/com/ezylang/evalex/operators/arithmetic/PrefixMinusOperator.java create mode 100644 src/main/java/com/ezylang/evalex/operators/arithmetic/PrefixPlusOperator.java create mode 100644 src/main/java/com/ezylang/evalex/operators/booleans/InfixAndOperator.java create mode 100644 src/main/java/com/ezylang/evalex/operators/booleans/InfixEqualsOperator.java create mode 100644 src/main/java/com/ezylang/evalex/operators/booleans/InfixGreaterEqualsOperator.java create mode 100644 src/main/java/com/ezylang/evalex/operators/booleans/InfixGreaterOperator.java create mode 100644 src/main/java/com/ezylang/evalex/operators/booleans/InfixLessEqualsOperator.java create mode 100644 src/main/java/com/ezylang/evalex/operators/booleans/InfixLessOperator.java create mode 100644 src/main/java/com/ezylang/evalex/operators/booleans/InfixNotEqualsOperator.java create mode 100644 src/main/java/com/ezylang/evalex/operators/booleans/InfixOrOperator.java create mode 100644 src/main/java/com/ezylang/evalex/operators/booleans/PrefixNotOperator.java create mode 100644 src/main/java/com/ezylang/evalex/parser/ASTNode.java create mode 100644 src/main/java/com/ezylang/evalex/parser/ParseException.java create mode 100644 src/main/java/com/ezylang/evalex/parser/ShuntingYardConverter.java create mode 100644 src/main/java/com/ezylang/evalex/parser/Token.java create mode 100644 src/main/java/com/ezylang/evalex/parser/Tokenizer.java diff --git a/dependencies.gradle b/dependencies.gradle index 522a72975..1442a3e24 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -31,13 +31,17 @@ dependencies { api("com.github.GTNewHorizons:GTNHLib:0.7.0:dev") - shadowImplementation('org.mariuszgromada.math:MathParser.org-mXparser:6.1.0') + // used in evalex + compileOnly("org.projectlombok:lombok:1.18.42") + annotationProcessor("org.projectlombok:lombok:1.18.42") + testCompileOnly("org.projectlombok:lombok:1.18.42") + testAnnotationProcessor("org.projectlombok:lombok:1.18.42") implementation("com.github.GTNewHorizons:NotEnoughItems:2.8.9-GTNH:dev") compileOnly("com.github.GTNewHorizons:Hodgepodge:2.7.2:dev") { transitive = false } compileOnly("com.github.GTNewHorizons:GT5-Unofficial:5.09.52.26:dev") { transitive = false } compileOnly("com.github.GTNewHorizons:ModularUI:1.3.0:dev") { transitive = false } - implementation("com.github.GTNewHorizons:Baubles-Expanded:2.2.0-GTNH:dev") { transitive = false } + implementation("com.github.GTNewHorizons:Baubles-Expanded:2.2.0-GTNH:dev") { transitive = false } compileOnly("thaumcraft:Thaumcraft:1.7.10-4.2.3.5:dev") //compileOnly rfg.deobf("curse.maven:neverenoughanimation-1062347:6552319") compileOnly files("libs/neverenoughanimations-v1.0.6-1-7-10.8+91c551807b-dirty-dev.jar") diff --git a/src/main/java/com/cleanroommc/modularui/ModularUI.java b/src/main/java/com/cleanroommc/modularui/ModularUI.java index 2a9d0b31b..f6c989ed4 100644 --- a/src/main/java/com/cleanroommc/modularui/ModularUI.java +++ b/src/main/java/com/cleanroommc/modularui/ModularUI.java @@ -12,7 +12,6 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.jetbrains.annotations.Nullable; -import org.mariuszgromada.math.mxparser.License; import java.util.function.Predicate; @@ -44,11 +43,6 @@ public class ModularUI { public static final boolean isTestEnv = Launch.blackboard == null; public static final boolean isDevEnv = isTestEnv || (boolean) Launch.blackboard.get("fml.deobfuscatedEnvironment"); - static { - // confirm mXparser license - License.iConfirmNonCommercialUse("GTNewHorizons"); - } - @Mod.EventHandler public void preInit(FMLPreInitializationEvent event) { proxy.preInit(event); @@ -69,7 +63,7 @@ public enum Mods { BAUBLES(ModIds.BAUBLES), BOGOSORTER(ModIds.BOGOSORTER), GT5U(ModIds.GT5U, mod -> !Loader.isModLoaded(ModIds.GT6)), - HODGEPODGE(ModIds.BOGOSORTER), + HODGEPODGE(ModIds.HODGEPODGE), NEI(ModIds.NEI), NEA(ModIds.NEA); diff --git a/src/main/java/com/cleanroommc/modularui/api/drawable/IDrawable.java b/src/main/java/com/cleanroommc/modularui/api/drawable/IDrawable.java index daea474cf..891ecf2c9 100644 --- a/src/main/java/com/cleanroommc/modularui/api/drawable/IDrawable.java +++ b/src/main/java/com/cleanroommc/modularui/api/drawable/IDrawable.java @@ -111,7 +111,6 @@ default void drawAtZeroPadded(GuiContext context, Area area, WidgetTheme widgetT draw(context, area.getPadding().getLeft(), area.getPadding().getTop(), area.paddedWidth(), area.paddedHeight(), widgetTheme); } - /** * @return if theme color can be applied on this drawable */ diff --git a/src/main/java/com/cleanroommc/modularui/utils/MathUtils.java b/src/main/java/com/cleanroommc/modularui/utils/MathUtils.java index 5d7fcefd4..4523a7a16 100644 --- a/src/main/java/com/cleanroommc/modularui/utils/MathUtils.java +++ b/src/main/java/com/cleanroommc/modularui/utils/MathUtils.java @@ -1,9 +1,16 @@ package com.cleanroommc.modularui.utils; +import com.cleanroommc.modularui.utils.math.PostfixPercentOperator; + import net.minecraft.util.MathHelper; -import org.mariuszgromada.math.mxparser.Constant; -import org.mariuszgromada.math.mxparser.Expression; +import com.ezylang.evalex.BaseException; +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.config.ExpressionConfiguration; +import com.ezylang.evalex.data.EvaluationValue; +import org.apache.commons.lang3.tuple.Pair; + +import java.math.BigDecimal; public class MathUtils { @@ -12,23 +19,12 @@ public class MathUtils { public static final float PI_HALF = PI / 2f; public static final float PI_QUART = PI / 4f; - // SI prefixes - public static final Constant k = new Constant("k", 1e3); - public static final Constant M = new Constant("M", 1e6); - public static final Constant G = new Constant("G", 1e9); - public static final Constant T = new Constant("T", 1e12); - public static final Constant P = new Constant("P", 1e15); - public static final Constant E = new Constant("E", 1e18); - public static final Constant Z = new Constant("Z", 1e21); - public static final Constant Y = new Constant("Y", 1e24); - public static final Constant m = new Constant("m", 1e-3); - public static final Constant u = new Constant("u", 1e-6); - public static final Constant n = new Constant("n", 1e-9); - public static final Constant p = new Constant("p", 1e-12); - public static final Constant f = new Constant("f", 1e-15); - public static final Constant a = new Constant("a", 1e-18); - public static final Constant z = new Constant("z", 1e-21); - public static final Constant y = new Constant("y", 1e-24); + public static final ExpressionConfiguration MATH_CFG = ExpressionConfiguration.builder() + .arraysAllowed(false) + .structuresAllowed(false) + .stripTrailingZeros(true) + .build() + .withAdditionalOperators(Pair.of("%", new PostfixPercentOperator())); public static ParseResult parseExpression(String expression) { return parseExpression(expression, Double.NaN, false); @@ -43,16 +39,19 @@ public static ParseResult parseExpression(String expression, double defaultValue } public static ParseResult parseExpression(String expression, double defaultValue, boolean useSiPrefixes) { - if (expression == null || expression.isEmpty()) return ParseResult.success(defaultValue); - Expression e = new Expression(expression); + if (expression == null || expression.isEmpty()) { + return ParseResult.success(EvaluationValue.numberValue(new BigDecimal(defaultValue))); + } + + Expression e = new Expression(expression, MATH_CFG); if (useSiPrefixes) { - e.addConstants(k, M, G, T, P, E, Z, Y, m, u, n, p, f, a, z, y); + SIPrefix.addAllToExpression(e); } - double result = e.calculate(); - if (Double.isNaN(result)) { - return ParseResult.failure(defaultValue, e.getErrorMessage()); + try { + return ParseResult.success(e.evaluate()); + } catch (BaseException exception) { + return ParseResult.failure(exception); } - return ParseResult.success(result); } public static int clamp(int v, int min, int max) { diff --git a/src/main/java/com/cleanroommc/modularui/utils/NumberFormat.java b/src/main/java/com/cleanroommc/modularui/utils/NumberFormat.java index 36860a56b..e61d728ee 100644 --- a/src/main/java/com/cleanroommc/modularui/utils/NumberFormat.java +++ b/src/main/java/com/cleanroommc/modularui/utils/NumberFormat.java @@ -1,5 +1,6 @@ package com.cleanroommc.modularui.utils; +import java.math.BigDecimal; import java.math.RoundingMode; import java.text.DecimalFormat; import java.text.DecimalFormatSymbols; @@ -10,6 +11,8 @@ */ public class NumberFormat { + public static final BigDecimal TEN_THOUSAND = new BigDecimal(10_000); + public static final Params DEFAULT = paramsBuilder() .roundingMode(RoundingMode.HALF_UP) .maxLength(4) @@ -205,6 +208,32 @@ public static SIPrefix findBestPrefix(double number) { return prefix; } + public static SIPrefix findBestPrefix(BigDecimal number) { + if (number.compareTo(BigDecimal.ONE) >= 0 && number.compareTo(TEN_THOUSAND) < 0) return SIPrefix.One; + SIPrefix[] high = SIPrefix.HIGH; + SIPrefix[] low = SIPrefix.LOW; + int n = high.length - 1; + SIPrefix prefix; + if (number.compareTo(TEN_THOUSAND) >= 0) { + int index; + for (index = 0; index < n; index++) { + if (number.compareTo(high[index + 1].bigFactor) < 0) { + break; + } + } + prefix = high[index]; + } else { + int index; + for (index = 0; index < n; index++) { + if (number.compareTo(low[index].bigFactor) >= 0) { + break; + } + } + prefix = low[index]; + } + return prefix; + } + private static String formatInternal(double number, int maxLength, Params params) { SIPrefix prefix = findBestPrefix(number); return formatToString(number * prefix.oneOverFactor, prefix.symbol, maxLength, params); diff --git a/src/main/java/com/cleanroommc/modularui/utils/ParseResult.java b/src/main/java/com/cleanroommc/modularui/utils/ParseResult.java index 12cc7bf7f..30ac3e252 100644 --- a/src/main/java/com/cleanroommc/modularui/utils/ParseResult.java +++ b/src/main/java/com/cleanroommc/modularui/utils/ParseResult.java @@ -1,25 +1,27 @@ package com.cleanroommc.modularui.utils; +import com.ezylang.evalex.BaseException; +import com.ezylang.evalex.data.EvaluationValue; import org.jetbrains.annotations.NotNull; public class ParseResult { - private final double result; - private final String error; + private final EvaluationValue result; + private final BaseException error; - public static ParseResult success(double result) { + public static ParseResult success(EvaluationValue result) { return new ParseResult(result, null); } - public static ParseResult failure(@NotNull String error) { - return failure(Double.NaN, error); + public static ParseResult failure(@NotNull BaseException error) { + return failure(null, error); } - public static ParseResult failure(double value, @NotNull String error) { + public static ParseResult failure(EvaluationValue value, @NotNull BaseException error) { return new ParseResult(value, error); } - private ParseResult(double result, String error) { + private ParseResult(EvaluationValue result, BaseException error) { this.result = result; this.error = error; } @@ -33,14 +35,21 @@ public boolean isFailure() { } public boolean hasValue() { - return !Double.isNaN(this.result); + return this.result != null; } - public double getResult() { + public EvaluationValue getResult() { return result; } - public String getError() { + public BaseException getError() { return error; } + + public String getErrorMessage() { + return isFailure() ? + String.format("%s for Token %s at %d:%d", + this.error.getMessage(), this.error.getTokenString(), + this.error.getStartPosition(), this.error.getEndPosition()) : null; + } } diff --git a/src/main/java/com/cleanroommc/modularui/utils/SIPrefix.java b/src/main/java/com/cleanroommc/modularui/utils/SIPrefix.java index 0f63c3640..7d9589f89 100644 --- a/src/main/java/com/cleanroommc/modularui/utils/SIPrefix.java +++ b/src/main/java/com/cleanroommc/modularui/utils/SIPrefix.java @@ -1,12 +1,16 @@ package com.cleanroommc.modularui.utils; +import com.ezylang.evalex.Expression; + +import java.math.BigDecimal; + public enum SIPrefix { Quetta('Q', 30), Ronna('R', 27), Yotta('Y', 24), Zetta('Z', 21), - Exa('E', 18), + Exa('X', 18), // this should actually be E, but this clashes with euler's number e = 2.71... Peta('P', 15), Tera('T', 12), Giga('G', 9), @@ -24,23 +28,31 @@ public enum SIPrefix { Ronto('r', -27), Quecto('q', -30); - public final char symbol; public final String stringSymbol; public final double factor; public final double oneOverFactor; + public final BigDecimal bigFactor; + public final BigDecimal bigOneOverFactor; SIPrefix(char symbol, int powerOfTen) { this.symbol = symbol; this.stringSymbol = symbol != Character.MIN_VALUE ? Character.toString(symbol) : ""; this.factor = Math.pow(10, powerOfTen); this.oneOverFactor = 1 / this.factor; + this.bigFactor = new BigDecimal(this.factor); + this.bigOneOverFactor = new BigDecimal(this.oneOverFactor); } public boolean isOne() { return this == One; } + public void addToExpression(Expression e) { + e.with(String.valueOf(this.symbol), this.factor); + } + + public static final SIPrefix[] VALUES = values(); public static final SIPrefix[] HIGH = new SIPrefix[values().length / 2]; public static final SIPrefix[] LOW = new SIPrefix[values().length / 2]; @@ -51,4 +63,10 @@ public boolean isOne() { LOW[i] = values[HIGH.length + 1 + i]; } } + + public static void addAllToExpression(Expression e) { + for (SIPrefix siPrefix : VALUES) { + siPrefix.addToExpression(e); + } + } } diff --git a/src/main/java/com/cleanroommc/modularui/utils/math/PostfixPercentOperator.java b/src/main/java/com/cleanroommc/modularui/utils/math/PostfixPercentOperator.java new file mode 100644 index 000000000..75f095638 --- /dev/null +++ b/src/main/java/com/cleanroommc/modularui/utils/math/PostfixPercentOperator.java @@ -0,0 +1,29 @@ +package com.cleanroommc.modularui.utils.math; + +import com.ezylang.evalex.EvaluationException; +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.operators.AbstractOperator; +import com.ezylang.evalex.operators.OperatorIfc; +import com.ezylang.evalex.operators.PostfixOperator; +import com.ezylang.evalex.parser.Token; + +import java.math.BigDecimal; + +@PostfixOperator(precedence = OperatorIfc.OPERATOR_PRECEDENCE_MULTIPLICATIVE - 1) +public class PostfixPercentOperator extends AbstractOperator { + + public static final BigDecimal HUNDRED = new BigDecimal(100); + + @Override + public EvaluationValue evaluate(Expression expression, Token operatorToken, EvaluationValue... operands) throws EvaluationException { + EvaluationValue operand = operands[0]; + + if (operand.isNumberValue()) { + return expression.convertValue( + operand.getNumberValue().divide(HUNDRED, expression.getConfiguration().getMathContext())); + } else { + throw EvaluationException.ofUnsupportedDataTypeInOperation(operatorToken); + } + } +} diff --git a/src/main/java/com/cleanroommc/modularui/widgets/textfield/TextFieldWidget.java b/src/main/java/com/cleanroommc/modularui/widgets/textfield/TextFieldWidget.java index 3073873ea..133fab6c0 100644 --- a/src/main/java/com/cleanroommc/modularui/widgets/textfield/TextFieldWidget.java +++ b/src/main/java/com/cleanroommc/modularui/widgets/textfield/TextFieldWidget.java @@ -41,12 +41,12 @@ public class TextFieldWidget extends BaseTextFieldWidget { public double parse(String num) { ParseResult result = MathUtils.parseExpression(num, this.defaultNumber, true); - double value = result.getResult(); if (result.isFailure()) { - this.mathFailMessage = result.getError(); + this.mathFailMessage = result.getErrorMessage(); ModularUI.LOGGER.error("Math expression error in {}: {}", this, this.mathFailMessage); + return defaultNumber; } - return value; + return result.getResult().getNumberValue().doubleValue(); } public IStringValue createMathFailMessageValue() { diff --git a/src/main/java/com/ezylang/evalex/BaseException.java b/src/main/java/com/ezylang/evalex/BaseException.java new file mode 100644 index 000000000..9ce958100 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/BaseException.java @@ -0,0 +1,42 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.ToString; + +/** + * Base exception class used in EvalEx. + */ +@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false) +@ToString +@Getter +public class BaseException extends Exception { + + @EqualsAndHashCode.Include private final int startPosition; + @EqualsAndHashCode.Include private final int endPosition; + @EqualsAndHashCode.Include private final String tokenString; + @EqualsAndHashCode.Include private final String message; + + public BaseException(int startPosition, int endPosition, String tokenString, String message) { + super(message); + this.startPosition = startPosition; + this.endPosition = endPosition; + this.tokenString = tokenString; + this.message = super.getMessage(); + } +} diff --git a/src/main/java/com/ezylang/evalex/EvaluationException.java b/src/main/java/com/ezylang/evalex/EvaluationException.java new file mode 100644 index 000000000..4f5509ee9 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/EvaluationException.java @@ -0,0 +1,36 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex; + +import com.ezylang.evalex.parser.Token; + +/** + * Exception while evaluating the parsed expression. + */ +public class EvaluationException extends BaseException { + + public EvaluationException(Token token, String message) { + super( + token.getStartPosition(), + token.getStartPosition() + token.getValue().length(), + token.getValue(), + message); + } + + public static EvaluationException ofUnsupportedDataTypeInOperation(Token token) { + return new EvaluationException(token, "Unsupported data types in operation"); + } +} diff --git a/src/main/java/com/ezylang/evalex/Expression.java b/src/main/java/com/ezylang/evalex/Expression.java new file mode 100644 index 000000000..d3c0263ef --- /dev/null +++ b/src/main/java/com/ezylang/evalex/Expression.java @@ -0,0 +1,469 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex; + +import com.ezylang.evalex.config.ExpressionConfiguration; +import com.ezylang.evalex.data.DataAccessorIfc; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.functions.FunctionIfc; +import com.ezylang.evalex.operators.OperatorIfc; +import com.ezylang.evalex.parser.ASTNode; +import com.ezylang.evalex.parser.ParseException; +import com.ezylang.evalex.parser.ShuntingYardConverter; +import com.ezylang.evalex.parser.Token; +import com.ezylang.evalex.parser.Tokenizer; +import lombok.Getter; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; +import java.util.TreeSet; + +/** + * Main class that allow creating, parsing, passing parameters and evaluating an expression string. + * + * @see EvalEx Homepage + */ +public class Expression { + + @Getter private final ExpressionConfiguration configuration; + + @Getter private final String expressionString; + + @Getter private final DataAccessorIfc dataAccessor; + + @Getter + private final Map constants = + new TreeMap<>(String.CASE_INSENSITIVE_ORDER); + + private ASTNode abstractSyntaxTree; + + /** + * Creates a new expression with the default configuration. The expression is not parsed until it + * is first evaluated or validated. + * + * @param expressionString A string holding an expression. + */ + public Expression(String expressionString) { + this(expressionString, ExpressionConfiguration.defaultConfiguration()); + } + + /** + * Creates a new expression with a custom configuration. The expression is not parsed until it is + * first evaluated or validated. + * + * @param expressionString A string holding an expression. + */ + public Expression(String expressionString, ExpressionConfiguration configuration) { + this.expressionString = expressionString; + this.configuration = configuration; + this.dataAccessor = configuration.getDataAccessorSupplier().get(); + this.constants.putAll(configuration.getDefaultConstants()); + } + + /** + * Creates a copy with the same expression string, configuration and syntax tree from an existing + * expression. The existing expression will be parsed to populate the syntax tree. + * + * @param expression An existing expression. + * @throws ParseException If there were problems while parsing the existing expression. + */ + public Expression(Expression expression) throws ParseException { + this(expression.getExpressionString(), expression.getConfiguration()); + this.abstractSyntaxTree = expression.getAbstractSyntaxTree(); + } + + /** + * Evaluates the expression by parsing it (if not done before) and the evaluating it. + * + * @return The evaluation result value. + * @throws EvaluationException If there were problems while evaluating the expression. + * @throws ParseException If there were problems while parsing the expression. + */ + public EvaluationValue evaluate() throws EvaluationException, ParseException { + EvaluationValue result = evaluateSubtree(getAbstractSyntaxTree(), 0); + if (result.isNumberValue()) { + BigDecimal bigDecimal = result.getNumberValue(); + if (configuration.getDecimalPlacesResult() + != ExpressionConfiguration.DECIMAL_PLACES_ROUNDING_UNLIMITED) { + bigDecimal = roundValue(bigDecimal, configuration.getDecimalPlacesResult()); + } + + if (configuration.isStripTrailingZeros()) { + bigDecimal = bigDecimal.stripTrailingZeros(); + } + + result = EvaluationValue.numberValue(bigDecimal); + } + + return result; + } + + /** + * Evaluates only a subtree of the abstract syntax tree. + * + * @param startNode The {@link ASTNode} to start evaluation from. + * @return The evaluation result value. + * @throws EvaluationException If there were problems while evaluating the expression. + */ + public EvaluationValue evaluateSubtree(ASTNode startNode) throws EvaluationException { + return evaluateSubtree(startNode, 0); + } + + /** + * Evaluates only a subtree of the abstract syntax tree. + * + * @param startNode The {@link ASTNode} to start evaluation from. + * @param depth The current depth, to track recursion level and secure it does not exceed the + * maximum level defined by {@link ExpressionConfiguration#getMaxRecursionDepth()} + * @return The evaluation result value. + * @throws EvaluationException If there were problems while evaluating the expression. + */ + private EvaluationValue evaluateSubtree(ASTNode startNode, int depth) throws EvaluationException { + if (depth > configuration.getMaxRecursionDepth()) { + throw new EvaluationException(startNode.getToken(), "Max recursion depth exceeded"); + } + + Token token = startNode.getToken(); + EvaluationValue result; + switch (token.getType()) { + case NUMBER_LITERAL: + result = EvaluationValue.numberOfString(token.getValue(), configuration.getMathContext()); + break; + case STRING_LITERAL: + result = EvaluationValue.stringValue(token.getValue()); + break; + case VARIABLE_OR_CONSTANT: + result = getVariableOrConstant(token); + if (result.isExpressionNode()) { + result = evaluateSubtree(result.getExpressionNode(), depth + 1); + } + break; + case PREFIX_OPERATOR: + case POSTFIX_OPERATOR: + result = + token + .getOperatorDefinition() + .evaluate( + this, token, evaluateSubtree(startNode.getParameters().get(0), depth + 1)); + break; + case INFIX_OPERATOR: + result = evaluateInfixOperator(startNode, token, depth + 1); + break; + case ARRAY_INDEX: + result = evaluateArrayIndex(startNode, depth + 1); + break; + case STRUCTURE_SEPARATOR: + result = evaluateStructureSeparator(startNode, depth + 1); + break; + case FUNCTION: + result = evaluateFunction(startNode, token, depth + 1); + break; + default: + throw new EvaluationException(token, "Unexpected evaluation token: " + token); + } + if (result.isNumberValue() + && configuration.getDecimalPlacesRounding() + != ExpressionConfiguration.DECIMAL_PLACES_ROUNDING_UNLIMITED) { + return EvaluationValue.numberValue( + roundValue(result.getNumberValue(), configuration.getDecimalPlacesRounding())); + } + + return result; + } + + private EvaluationValue getVariableOrConstant(Token token) throws EvaluationException { + EvaluationValue result = constants.get(token.getValue()); + if (result == null) { + result = getDataAccessor().getData(token.getValue()); + } + if (result == null) { + if (configuration.isLenientMode()) { + return EvaluationValue.UNDEFINED; + } + throw new EvaluationException( + token, String.format("Variable or constant value for '%s' not found", token.getValue())); + } + return result; + } + + private EvaluationValue evaluateFunction(ASTNode startNode, Token token, int depth) + throws EvaluationException { + List parameterResults = new ArrayList<>(); + for (int i = 0; i < startNode.getParameters().size(); i++) { + if (token.getFunctionDefinition().isParameterLazy(i)) { + parameterResults.add(convertValue(startNode.getParameters().get(i))); + } else { + parameterResults.add(evaluateSubtree(startNode.getParameters().get(i), depth + 1)); + } + } + + EvaluationValue[] parameters = parameterResults.toArray(new EvaluationValue[0]); + + FunctionIfc function = token.getFunctionDefinition(); + + function.validatePreEvaluation(token, parameters); + + return function.evaluate(this, token, parameters); + } + + private EvaluationValue evaluateArrayIndex(ASTNode startNode, int depth) + throws EvaluationException { + EvaluationValue array = evaluateSubtree(startNode.getParameters().get(0), depth + 1); + EvaluationValue index = evaluateSubtree(startNode.getParameters().get(1), depth + 1); + + if (array.isArrayValue() && index.isNumberValue()) { + if (index.getNumberValue().intValue() < 0 + || index.getNumberValue().intValue() >= array.getArrayValue().size()) { + throw new EvaluationException( + startNode.getToken(), + String.format( + "Index %d out of bounds for array of length %d", + index.getNumberValue().intValue(), array.getArrayValue().size())); + } + return array.getArrayValue().get(index.getNumberValue().intValue()); + } else { + throw EvaluationException.ofUnsupportedDataTypeInOperation(startNode.getToken()); + } + } + + private EvaluationValue evaluateStructureSeparator(ASTNode startNode, int depth) + throws EvaluationException { + EvaluationValue structure = evaluateSubtree(startNode.getParameters().get(0), depth + 1); + Token nameToken = startNode.getParameters().get(1).getToken(); + String name = nameToken.getValue(); + + if (structure.isStructureValue()) { + if (!structure.getStructureValue().containsKey(name)) { + throw new EvaluationException( + nameToken, String.format("Field '%s' not found in structure", name)); + } + return structure.getStructureValue().get(name); + } else { + throw EvaluationException.ofUnsupportedDataTypeInOperation(startNode.getToken()); + } + } + + private EvaluationValue evaluateInfixOperator(ASTNode startNode, Token token, int depth) + throws EvaluationException { + EvaluationValue left; + EvaluationValue right; + + OperatorIfc op = token.getOperatorDefinition(); + if (op.isOperandLazy()) { + left = convertValue(startNode.getParameters().get(0)); + right = convertValue(startNode.getParameters().get(1)); + } else { + left = evaluateSubtree(startNode.getParameters().get(0), depth + 1); + right = evaluateSubtree(startNode.getParameters().get(1), depth + 1); + } + return op.evaluate(this, token, left, right); + } + + /** + * Rounds the given value. + * + * @param value The input value. + * @param decimalPlaces The number of decimal places to round to. + * @return The rounded value, or the input value if rounding is not configured or possible. + */ + private BigDecimal roundValue(BigDecimal value, int decimalPlaces) { + value = value.setScale(decimalPlaces, configuration.getMathContext().getRoundingMode()); + return value; + } + + /** + * Returns the root ode of the parsed abstract syntax tree. + * + * @return The abstract syntax tree root node. + * @throws ParseException If there were problems while parsing the expression. + */ + public ASTNode getAbstractSyntaxTree() throws ParseException { + if (abstractSyntaxTree == null) { + Tokenizer tokenizer = new Tokenizer(expressionString, configuration); + ShuntingYardConverter converter = + new ShuntingYardConverter(expressionString, tokenizer.parse(), configuration); + abstractSyntaxTree = converter.toAbstractSyntaxTree(); + } + + return abstractSyntaxTree; + } + + /** + * Validates the expression by parsing it and throwing an exception, if the parser fails. + * + * @throws ParseException If there were problems while parsing the expression. + */ + public void validate() throws ParseException { + getAbstractSyntaxTree(); + } + + /** + * Adds a variable value to the expression data storage. If a value with the same name already + * exists, it is overridden. The data type will be determined by examining the passed value + * object. An exception is thrown, if he found data type is not supported. + * + * @param variable The variable name. + * @param value The variable value. + * @return The Expression instance, to allow chaining of methods. + */ + public Expression with(String variable, Object value) { + if (constants.containsKey(variable)) { + if (configuration.isAllowOverwriteConstants()) { + constants.remove(variable); + } else { + throw new UnsupportedOperationException( + String.format("Can't set value for constant '%s'", variable)); + } + } + getDataAccessor().setData(variable, convertValue(value)); + return this; + } + + /** + * Adds a variable value to the expression data storage. If a value with the same name already + * exists, it is overridden. The data type will be determined by examining the passed value + * object. An exception is thrown, if he found data type is not supported. + * + * @param variable The variable name. + * @param value The variable value. + * @return The Expression instance, to allow chaining of methods. + */ + public Expression and(String variable, Object value) { + return with(variable, value); + } + + /** + * Adds all variables values defined in the map with their name (key) and value to the data + * storage.If a value with the same name already exists, it is overridden. The data type will be + * determined by examining the passed value object. An exception is thrown, if he found data type + * is not supported. + * + * @param values A map with variable values. + * @return The Expression instance, to allow chaining of methods. + */ + public Expression withValues(Map values) { + for (Map.Entry entry : values.entrySet()) { + with(entry.getKey(), entry.getValue()); + } + return this; + } + + /** + * Return a copy of the expression using the copy constructor {@link Expression(Expression)}. + * + * @return The copied Expression instance. + * @throws ParseException If there were problems while parsing the existing expression. + */ + public Expression copy() throws ParseException { + return new Expression(this); + } + + /** + * Create an AST representation for an expression string. The node can then be used as a + * sub-expression. Subexpressions are not cached. + * + * @param expression The expression string. + * @return The root node of the expression AST representation. + * @throws ParseException On any parsing error. + */ + public ASTNode createExpressionNode(String expression) throws ParseException { + Tokenizer tokenizer = new Tokenizer(expression, configuration); + ShuntingYardConverter converter = + new ShuntingYardConverter(expression, tokenizer.parse(), configuration); + return converter.toAbstractSyntaxTree(); + } + + /** + * Converts a double value to an {@link EvaluationValue} by considering the configured {@link + * java.math.MathContext}. + * + * @param value The double value to covert. + * @return An {@link EvaluationValue} of type {@link EvaluationValue.DataType#NUMBER}. + */ + public EvaluationValue convertDoubleValue(double value) { + return convertValue(value); + } + + /** + * Converts an object value to an {@link EvaluationValue} by considering the configuration {@link + * EvaluationValue(Object, ExpressionConfiguration)}. + * + * @param value The object value to covert. + * @return An {@link EvaluationValue} of the detected type and value. + */ + public EvaluationValue convertValue(Object value) { + return EvaluationValue.of(value, configuration); + } + + /** + * Returns the list of all nodes of the abstract syntax tree. + * + * @return The list of all nodes in the parsed expression. + * @throws ParseException If there were problems while parsing the expression. + */ + public List getAllASTNodes() throws ParseException { + return getAllASTNodesForNode(getAbstractSyntaxTree()); + } + + private List getAllASTNodesForNode(ASTNode node) { + List nodes = new ArrayList<>(); + nodes.add(node); + for (ASTNode child : node.getParameters()) { + nodes.addAll(getAllASTNodesForNode(child)); + } + return nodes; + } + + /** + * Returns all variables that are used i the expression, excluding the constants like e.g. + * PI or TRUE and FALSE. + * + * @return All used variables excluding constants. + * @throws ParseException If there were problems while parsing the expression. + */ + public Set getUsedVariables() throws ParseException { + Set variables = new TreeSet<>(String.CASE_INSENSITIVE_ORDER); + + for (ASTNode node : getAllASTNodes()) { + if (node.getToken().getType() == Token.TokenType.VARIABLE_OR_CONSTANT + && !constants.containsKey(node.getToken().getValue())) { + variables.add(node.getToken().getValue()); + } + } + + return variables; + } + + /** + * Returns all variables that are used in the expression, but have no value assigned. + * + * @return All variables that have no value assigned. + * @throws ParseException If there were problems while parsing the expression. + */ + public Set getUndefinedVariables() throws ParseException { + Set variables = new TreeSet<>(String.CASE_INSENSITIVE_ORDER); + for (String variable : getUsedVariables()) { + if (getDataAccessor().getData(variable) == null) { + variables.add(variable); + } + } + return variables; + } +} diff --git a/src/main/java/com/ezylang/evalex/LICENSE b/src/main/java/com/ezylang/evalex/LICENSE new file mode 100644 index 000000000..261eeb9e9 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/src/main/java/com/ezylang/evalex/README.md b/src/main/java/com/ezylang/evalex/README.md new file mode 100644 index 000000000..ef43839cc --- /dev/null +++ b/src/main/java/com/ezylang/evalex/README.md @@ -0,0 +1,298 @@ +EvalEx - Java Expression Evaluator +========== + +[![Build](https://github.com/ezylang/EvalEx/actions/workflows/build.yml/badge.svg)](https://github.com/ezylang/EvalEx/actions/workflows/build.yml) +[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=ezylang_EvalEx&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=ezylang_EvalEx) +[![Security Rating](https://sonarcloud.io/api/project_badges/measure?project=ezylang_EvalEx&metric=security_rating)](https://sonarcloud.io/summary/new_code?id=ezylang_EvalEx) +[![Vulnerabilities](https://sonarcloud.io/api/project_badges/measure?project=ezylang_EvalEx&metric=vulnerabilities)](https://sonarcloud.io/summary/new_code?id=ezylang_EvalEx) +[![Maven Central](https://img.shields.io/maven-central/v/com.ezylang/EvalEx.svg?label=Maven%20Central)](https://search.maven.org/search?q=a:%22EvalEx%22) + +| For a complete documentation, see [the documentation site](https://ezylang.github.io/EvalEx/). | +|------------------------------------------------------------------------------------------------| + +EvalEx is a handy expression evaluator for Java, that allows to parse and evaluate expression strings. + +## Key Features: + +- Supports numerical, boolean, string, date time, duration, array and structure expressions, operations and variables. +- Array and structure support: Arrays and structures can be mixed, building arbitrary data + structures. +- Supports the NULL datatype. +- Uses BigDecimal for numerical calculations. +- MathContext and number of decimal places can be configured, with optional automatic rounding. +- No dependencies to external libraries. +- Easy integration into existing systems to access data. +- Predefined boolean and mathematical operators. +- Predefined mathematical, boolean and string functions. +- Custom functions and operators can be added. +- Functions can be defined with a variable number of arguments (see MIN, MAX and SUM functions). +- Supports hexadecimal and scientific notations of numbers. +- Supports implicit multiplication, e.g. 2x, 2sin(x), (a+b)(a-b) or 2(x-y) which equals to (a+b)\*(a-b) or 2\*( + x-y) +- Lazy evaluation of function parameters (see the IF function) and support of sub-expressions. +- Requires minimum Java version 11. + +## Documentation + +The full documentation for EvalEx can be found +on [GitHub Pages](https://ezylang.github.io/EvalEx/) + +## Discussion + +For announcements, questions and ideas visit +the [Discussions area](https://github.com/ezylang/EvalEx/discussions). + +## Download / Including + +You can download the binaries, source code and JavaDoc jars from +[Maven Central](https://central.sonatype.com/artifact/com.ezylang/EvalEx).\ +You will find there also copy/paste templates for including EvalEx in your project with build +systems like Maven or Gradle. + +### Maven + +To include it in your Maven project, add the dependency to your pom. For example: + +```xml + + + + com.ezylang + EvalEx + 3.5.0 + + +``` + +### Gradle + +If you're using gradle add the dependencies to your project's app build.gradle: + +```gradle +dependencies { + compile 'com.ezylang:EvalEx:3.5.0' +} +``` + +## Examples + +### A simple example, that shows how it works in general: + +```java +Expression expression = new Expression("1 + 2 / (4 * SQRT(4))"); + +EvaluationValue result = expression.evaluate(); + +System.out. + +println(result.getNumberValue()); // prints 1.25 +``` + +### Variables can be specified in the expression and their values can be passed for evaluation: + +```java +Expression expression = new Expression("(a + b) * (a - b)"); + +EvaluationValue result = expression + .with("a", 3.5) + .and("b", 2.5) + .evaluate(); + +System.out. + +println(result.getNumberValue()); // prints 6.00 +``` + +### Expression can be copied and evaluated with a different set of values: + +Using a copy of the expression allows a thread-safe evaluation of that copy, without parsing the expression again. +The copy uses the same expression string, configuration and syntax tree. +The existing expression will be parsed to populate the syntax tree. + +Make sure each thread has its own copy of the original expression. + +```java +Expression expression = new Expression("a + b").with("a", 1).and("b", 2); +Expression copiedExpression = expression.copy().with("a", 3).and("b", 4); + +EvaluationValue result = expression.evaluate(); +EvaluationValue copiedResult = copiedExpression.evaluate(); + +System.out. + +println(result.getNumberValue()); // prints 3 + System.out. + +println(copiedResult.getNumberValue()); // prints 7 +``` + +### Values can be passed in a map + +Instead of specifying the variable values one by one, they can be set by defining a map with names and values and then +passing it to the _withValues()_ method: + +The data conversion of the passed values will automatically be performed through a customizable converter. + +It is also possible to configure a custom data accessor to read and write values. + +```java +Expression expression = new Expression("a+b+c"); + +Map values = new HashMap<>(); +values. + +put("a",true); +values. + +put("b"," : "); +values. + +put("c",24.7); + +EvaluationValue result = expression.withValues(values).evaluate(); + +System.out. + +println(result.getStringValue()); // prints "true : 24.7" +``` + +See chapter [Data Types](https://ezylang.github.io/EvalEx/concepts/datatypes.html) for details on the conversion. + +Another option to have EvalEx use your data is to define a custom data accessor. + +See chapter [Data Access](https://ezylang.github.io/EvalEx/customization/data_access.html) for details. + +### Boolean expressions produce a boolean result: + +```java +Expression expression = new Expression("level > 2 || level <= 0"); + +EvaluationValue result = expression + .with("level", 3.5) + .evaluate(); + +System.out. + +println(result.getBooleanValue()); // prints true +``` + +### Like in Java, strings and text can be mixed: + +```java +Expression expression = new Expression("\"Hello \" + name + \", you are \" + age") + .with("name", "Frank") + .and("age", 38); + +System.out. + +println(expression.evaluate(). + +getStringValue()); // prints Hello Frank, you are 38 +``` + +### Arrays (also multidimensional) are supported and can be passed as Java _Lists_ or instances of Java arrays. + +See the [Documentation](https://ezylang.github.io/EvalEx/concepts/datatypes.html#array) +for more details. + +```java +Expression expression = new Expression("values[i-1] * factors[i-1]"); + +EvaluationValue result = expression + .with("values", List.of(2, 3, 4)) + .and("factors", new Object[]{2, 4, 6}) + .and("i", 1) + .evaluate(); + +System.out. + +println(result.getNumberValue()); // prints 4 +``` + +### Structures are supported and can be passed as Java _Maps_. + +Arrays and Structures can be combined to build arbitrary data structures. See +the [Documentation](https://ezylang.github.io/EvalEx/concepts/datatypes.html#structure) +for more details. + +```java +Map order = new HashMap<>(); +order. + +put("id",12345); +order. + +put("name","Mary"); + +Map position = new HashMap<>(); +position. + +put("article",3114); +position. + +put("amount",3); +position. + +put("price",new BigDecimal("14.95")); + + order. + +put("positions",List.of(position)); + +Expression expression = new Expression("order.positions[x].amount * order.positions[x].price") + .with("order", order) + .and("x", 0); + +BigDecimal result = expression.evaluate().getNumberValue(); + +System.out. + +println(result); // prints 44.85 +``` + +### Calculating with date-time and duration + +Date-tme and duration values are supported. There are functions to create, parse and format these values. +Additionally, the plus and minus operators can be used to e.g. add or subtract durations, or to calculate the +difference between two dates: + +```java +Instant start = Instant.parse("2023-12-05T11:20:00.00Z"); +Instant end = Instant.parse("2023-12-04T23:15:30.00Z"); + +Expression expression = new Expression("start - end"); +EvaluationValue result = expression + .with("start", start) + .and("end", end) + .evaluate(); +System.out. + +println(result); // will print "EvaluationValue(value=PT12H4M30S, dataType=DURATION)" +``` + +See the [Documentation](https://ezylang.github.io/EvalEx/concepts/date_time_duration.html) for more details. + +## EvalEx-big-math + +[Big-math](https://github.com/eobermuhlner/big-math) is a library by Eric Obermühlner. It provides +advanced Java BigDecimal math functions using an arbitrary precision. + +[EvalEx-big-math](https://github.com/ezylang/EvalEx-big-math) adds the advanced math functions from +big-math to EvalEx. + +## Author and License + +Copyright 2012-2023 by Udo Klimaschewski + +**Thanks to all who contributed to this +project: [Contributors](https://github.com/ezylang/EvalEx/graphs/contributors)** + +The software is licensed under the Apache License, Version 2.0 ( +see [LICENSE](https://raw.githubusercontent.com/ezylang/EvalEx/main/LICENSE) file). + +* The *power of* operator (^) implementation was copied + from [Stack Overflow](http://stackoverflow.com/questions/3579779/how-to-do-a-fractional-power-on-bigdecimal-in-java) + Thanks to Gene Marin +* The SQRT() function implementation was taken from the + book [The Java Programmers Guide To numerical Computing](http://www.amazon.de/Java-Number-Cruncher-Programmers-Numerical/dp/0130460419) ( + Ronald Mak, 2002) diff --git a/src/main/java/com/ezylang/evalex/config/ExpressionConfiguration.java b/src/main/java/com/ezylang/evalex/config/ExpressionConfiguration.java new file mode 100644 index 000000000..f2ae40c29 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/config/ExpressionConfiguration.java @@ -0,0 +1,514 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.config; + +import com.ezylang.evalex.data.DataAccessorIfc; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.data.MapBasedDataAccessor; +import com.ezylang.evalex.data.conversion.DefaultEvaluationValueConverter; +import com.ezylang.evalex.data.conversion.EvaluationValueConverterIfc; +import com.ezylang.evalex.functions.FunctionIfc; +import com.ezylang.evalex.functions.basic.AbsFunction; +import com.ezylang.evalex.functions.basic.AverageFunction; +import com.ezylang.evalex.functions.basic.CeilingFunction; +import com.ezylang.evalex.functions.basic.CoalesceFunction; +import com.ezylang.evalex.functions.basic.FactFunction; +import com.ezylang.evalex.functions.basic.FloorFunction; +import com.ezylang.evalex.functions.basic.IfFunction; +import com.ezylang.evalex.functions.basic.Log10Function; +import com.ezylang.evalex.functions.basic.LogFunction; +import com.ezylang.evalex.functions.basic.MaxFunction; +import com.ezylang.evalex.functions.basic.MinFunction; +import com.ezylang.evalex.functions.basic.NotFunction; +import com.ezylang.evalex.functions.basic.RandomFunction; +import com.ezylang.evalex.functions.basic.RoundFunction; +import com.ezylang.evalex.functions.basic.SqrtFunction; +import com.ezylang.evalex.functions.basic.SumFunction; +import com.ezylang.evalex.functions.basic.SwitchFunction; +import com.ezylang.evalex.functions.datetime.DateTimeFormatFunction; +import com.ezylang.evalex.functions.datetime.DateTimeNewFunction; +import com.ezylang.evalex.functions.datetime.DateTimeNowFunction; +import com.ezylang.evalex.functions.datetime.DateTimeParseFunction; +import com.ezylang.evalex.functions.datetime.DateTimeToEpochFunction; +import com.ezylang.evalex.functions.datetime.DateTimeTodayFunction; +import com.ezylang.evalex.functions.datetime.DurationFromMillisFunction; +import com.ezylang.evalex.functions.datetime.DurationNewFunction; +import com.ezylang.evalex.functions.datetime.DurationParseFunction; +import com.ezylang.evalex.functions.datetime.DurationToMillisFunction; +import com.ezylang.evalex.functions.string.StringContains; +import com.ezylang.evalex.functions.string.StringEndsWithFunction; +import com.ezylang.evalex.functions.string.StringFormatFunction; +import com.ezylang.evalex.functions.string.StringLeftFunction; +import com.ezylang.evalex.functions.string.StringLengthFunction; +import com.ezylang.evalex.functions.string.StringLowerFunction; +import com.ezylang.evalex.functions.string.StringMatchesFunction; +import com.ezylang.evalex.functions.string.StringRightFunction; +import com.ezylang.evalex.functions.string.StringSplitFunction; +import com.ezylang.evalex.functions.string.StringStartsWithFunction; +import com.ezylang.evalex.functions.string.StringSubstringFunction; +import com.ezylang.evalex.functions.string.StringTrimFunction; +import com.ezylang.evalex.functions.string.StringUpperFunction; +import com.ezylang.evalex.functions.trigonometric.AcosFunction; +import com.ezylang.evalex.functions.trigonometric.AcosHFunction; +import com.ezylang.evalex.functions.trigonometric.AcosRFunction; +import com.ezylang.evalex.functions.trigonometric.AcotFunction; +import com.ezylang.evalex.functions.trigonometric.AcotHFunction; +import com.ezylang.evalex.functions.trigonometric.AcotRFunction; +import com.ezylang.evalex.functions.trigonometric.AsinFunction; +import com.ezylang.evalex.functions.trigonometric.AsinHFunction; +import com.ezylang.evalex.functions.trigonometric.AsinRFunction; +import com.ezylang.evalex.functions.trigonometric.Atan2Function; +import com.ezylang.evalex.functions.trigonometric.Atan2RFunction; +import com.ezylang.evalex.functions.trigonometric.AtanFunction; +import com.ezylang.evalex.functions.trigonometric.AtanHFunction; +import com.ezylang.evalex.functions.trigonometric.AtanRFunction; +import com.ezylang.evalex.functions.trigonometric.CosFunction; +import com.ezylang.evalex.functions.trigonometric.CosHFunction; +import com.ezylang.evalex.functions.trigonometric.CosRFunction; +import com.ezylang.evalex.functions.trigonometric.CotFunction; +import com.ezylang.evalex.functions.trigonometric.CotHFunction; +import com.ezylang.evalex.functions.trigonometric.CotRFunction; +import com.ezylang.evalex.functions.trigonometric.CscFunction; +import com.ezylang.evalex.functions.trigonometric.CscHFunction; +import com.ezylang.evalex.functions.trigonometric.CscRFunction; +import com.ezylang.evalex.functions.trigonometric.DegFunction; +import com.ezylang.evalex.functions.trigonometric.RadFunction; +import com.ezylang.evalex.functions.trigonometric.SecFunction; +import com.ezylang.evalex.functions.trigonometric.SecHFunction; +import com.ezylang.evalex.functions.trigonometric.SecRFunction; +import com.ezylang.evalex.functions.trigonometric.SinFunction; +import com.ezylang.evalex.functions.trigonometric.SinHFunction; +import com.ezylang.evalex.functions.trigonometric.SinRFunction; +import com.ezylang.evalex.functions.trigonometric.TanFunction; +import com.ezylang.evalex.functions.trigonometric.TanHFunction; +import com.ezylang.evalex.functions.trigonometric.TanRFunction; +import com.ezylang.evalex.operators.OperatorIfc; +import com.ezylang.evalex.operators.arithmetic.InfixDivisionOperator; +import com.ezylang.evalex.operators.arithmetic.InfixMinusOperator; +import com.ezylang.evalex.operators.arithmetic.InfixModuloOperator; +import com.ezylang.evalex.operators.arithmetic.InfixMultiplicationOperator; +import com.ezylang.evalex.operators.arithmetic.InfixPlusOperator; +import com.ezylang.evalex.operators.arithmetic.InfixPowerOfOperator; +import com.ezylang.evalex.operators.arithmetic.PrefixMinusOperator; +import com.ezylang.evalex.operators.arithmetic.PrefixPlusOperator; +import com.ezylang.evalex.operators.booleans.InfixAndOperator; +import com.ezylang.evalex.operators.booleans.InfixEqualsOperator; +import com.ezylang.evalex.operators.booleans.InfixGreaterEqualsOperator; +import com.ezylang.evalex.operators.booleans.InfixGreaterOperator; +import com.ezylang.evalex.operators.booleans.InfixLessEqualsOperator; +import com.ezylang.evalex.operators.booleans.InfixLessOperator; +import com.ezylang.evalex.operators.booleans.InfixNotEqualsOperator; +import com.ezylang.evalex.operators.booleans.InfixOrOperator; +import com.ezylang.evalex.operators.booleans.PrefixNotOperator; +import lombok.Builder; +import lombok.Getter; +import org.apache.commons.lang3.tuple.Pair; + +import java.math.BigDecimal; +import java.math.MathContext; +import java.math.RoundingMode; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.TreeMap; +import java.util.function.Supplier; + +/** + * The expression configuration can be used to configure various aspects of expression parsing and + * evaluation.
+ * A Builder is provided to create custom configurations, e.g.:
+ * + *
+ *   ExpressionConfiguration config = ExpressionConfiguration.builder().mathContext(MathContext.DECIMAL32).arraysAllowed(false).build();
+ * 
+ * + *
+ * Additional operators and functions can be added to an existing configuration:
+ * + *
+ *     ExpressionConfiguration.defaultConfiguration()
+ *        .withAdditionalOperators(
+ *            Pair.of("++", new PrefixPlusPlusOperator()),
+ *            Pair.of("++", new PostfixPlusPlusOperator()))
+ *        .withAdditionalFunctions(Pair.of("save", new SaveFunction()),
+ *            Pair.of("update", new UpdateFunction()));
+ * 
+ */ +@Builder(toBuilder = true) +@Getter +public class ExpressionConfiguration { + + /** + * The standard set constants for EvalEx. + */ + public static final Map StandardConstants = + Collections.unmodifiableMap(getStandardConstants()); + + /** + * Setting the decimal places to unlimited, will disable intermediate rounding. + */ + public static final int DECIMAL_PLACES_ROUNDING_UNLIMITED = -1; + + /** + * The default math context has a precision of 68 and {@link RoundingMode#HALF_EVEN}. + */ + public static final MathContext DEFAULT_MATH_CONTEXT = + new MathContext(68, RoundingMode.HALF_EVEN); + + /** + * The default maximum depth for recursion is 2000 levels. + */ + public static final int DEFAULT_MAX_RECURSION_DEPTH = 2_000; + + /** + * The default date time formatters used when parsing a date string. Each format will be tried and + * the first matching will be used. + * + *
    + *
  • {@link DateTimeFormatter#ISO_DATE_TIME} + *
  • {@link DateTimeFormatter#ISO_DATE} + *
  • {@link DateTimeFormatter#ISO_LOCAL_DATE_TIME} + *
  • {@link DateTimeFormatter#ISO_LOCAL_DATE} + *
+ */ + protected static final List DEFAULT_DATE_TIME_FORMATTERS = + new ArrayList<>( + Arrays.asList( + DateTimeFormatter.ISO_DATE_TIME, + DateTimeFormatter.ISO_DATE, + DateTimeFormatter.ISO_LOCAL_DATE_TIME, + DateTimeFormatter.ISO_LOCAL_DATE, + DateTimeFormatter.RFC_1123_DATE_TIME)); + + /** + * The operator dictionary holds all operators that will be allowed in an expression. + */ + @Builder.Default + @SuppressWarnings("unchecked") + private final OperatorDictionaryIfc operatorDictionary = + MapBasedOperatorDictionary.ofOperators( + // arithmetic + Pair.of("+", new PrefixPlusOperator()), + Pair.of("-", new PrefixMinusOperator()), + Pair.of("+", new InfixPlusOperator()), + Pair.of("-", new InfixMinusOperator()), + Pair.of("*", new InfixMultiplicationOperator()), + Pair.of("/", new InfixDivisionOperator()), + Pair.of("^", new InfixPowerOfOperator()), + Pair.of("%", new InfixModuloOperator()), + // booleans + Pair.of("=", new InfixEqualsOperator()), + Pair.of("==", new InfixEqualsOperator()), + Pair.of("!=", new InfixNotEqualsOperator()), + Pair.of("<>", new InfixNotEqualsOperator()), + Pair.of(">", new InfixGreaterOperator()), + Pair.of(">=", new InfixGreaterEqualsOperator()), + Pair.of("<", new InfixLessOperator()), + Pair.of("<=", new InfixLessEqualsOperator()), + Pair.of("&&", new InfixAndOperator()), + Pair.of("||", new InfixOrOperator()), + Pair.of("!", new PrefixNotOperator())); + + /** + * The function dictionary holds all functions that will be allowed in an expression. + */ + @Builder.Default + @SuppressWarnings("unchecked") + private final FunctionDictionaryIfc functionDictionary = + MapBasedFunctionDictionary.ofFunctions( + // basic functions + Pair.of("ABS", new AbsFunction()), + Pair.of("AVERAGE", new AverageFunction()), + Pair.of("CEILING", new CeilingFunction()), + Pair.of("COALESCE", new CoalesceFunction()), + Pair.of("FACT", new FactFunction()), + Pair.of("FLOOR", new FloorFunction()), + Pair.of("IF", new IfFunction()), + Pair.of("LOG", new LogFunction()), + Pair.of("LOG10", new Log10Function()), + Pair.of("MAX", new MaxFunction()), + Pair.of("MIN", new MinFunction()), + Pair.of("NOT", new NotFunction()), + Pair.of("RANDOM", new RandomFunction()), + Pair.of("ROUND", new RoundFunction()), + Pair.of("SQRT", new SqrtFunction()), + Pair.of("SUM", new SumFunction()), + Pair.of("SWITCH", new SwitchFunction()), + // trigonometric + Pair.of("ACOS", new AcosFunction()), + Pair.of("ACOSH", new AcosHFunction()), + Pair.of("ACOSR", new AcosRFunction()), + Pair.of("ACOT", new AcotFunction()), + Pair.of("ACOTH", new AcotHFunction()), + Pair.of("ACOTR", new AcotRFunction()), + Pair.of("ASIN", new AsinFunction()), + Pair.of("ASINH", new AsinHFunction()), + Pair.of("ASINR", new AsinRFunction()), + Pair.of("ATAN", new AtanFunction()), + Pair.of("ATAN2", new Atan2Function()), + Pair.of("ATAN2R", new Atan2RFunction()), + Pair.of("ATANH", new AtanHFunction()), + Pair.of("ATANR", new AtanRFunction()), + Pair.of("COS", new CosFunction()), + Pair.of("COSH", new CosHFunction()), + Pair.of("COSR", new CosRFunction()), + Pair.of("COT", new CotFunction()), + Pair.of("COTH", new CotHFunction()), + Pair.of("COTR", new CotRFunction()), + Pair.of("CSC", new CscFunction()), + Pair.of("CSCH", new CscHFunction()), + Pair.of("CSCR", new CscRFunction()), + Pair.of("DEG", new DegFunction()), + Pair.of("RAD", new RadFunction()), + Pair.of("SIN", new SinFunction()), + Pair.of("SINH", new SinHFunction()), + Pair.of("SINR", new SinRFunction()), + Pair.of("SEC", new SecFunction()), + Pair.of("SECH", new SecHFunction()), + Pair.of("SECR", new SecRFunction()), + Pair.of("TAN", new TanFunction()), + Pair.of("TANH", new TanHFunction()), + Pair.of("TANR", new TanRFunction()), + // string functions + Pair.of("STR_CONTAINS", new StringContains()), + Pair.of("STR_ENDS_WITH", new StringEndsWithFunction()), + Pair.of("STR_FORMAT", new StringFormatFunction()), + Pair.of("STR_LEFT", new StringLeftFunction()), + Pair.of("STR_LENGTH", new StringLengthFunction()), + Pair.of("STR_LOWER", new StringLowerFunction()), + Pair.of("STR_MATCHES", new StringMatchesFunction()), + Pair.of("STR_RIGHT", new StringRightFunction()), + Pair.of("STR_SPLIT", new StringSplitFunction()), + Pair.of("STR_STARTS_WITH", new StringStartsWithFunction()), + Pair.of("STR_SUBSTRING", new StringSubstringFunction()), + Pair.of("STR_TRIM", new StringTrimFunction()), + Pair.of("STR_UPPER", new StringUpperFunction()), + // date time functions + Pair.of("DT_DATE_NEW", new DateTimeNewFunction()), + Pair.of("DT_DATE_PARSE", new DateTimeParseFunction()), + Pair.of("DT_DATE_FORMAT", new DateTimeFormatFunction()), + Pair.of("DT_DATE_TO_EPOCH", new DateTimeToEpochFunction()), + Pair.of("DT_DURATION_NEW", new DurationNewFunction()), + Pair.of("DT_DURATION_FROM_MILLIS", new DurationFromMillisFunction()), + Pair.of("DT_DURATION_TO_MILLIS", new DurationToMillisFunction()), + Pair.of("DT_DURATION_PARSE", new DurationParseFunction()), + Pair.of("DT_NOW", new DateTimeNowFunction()), + Pair.of("DT_TODAY", new DateTimeTodayFunction())); + + /** + * The math context to use. + */ + @Builder.Default private final MathContext mathContext = DEFAULT_MATH_CONTEXT; + + /** + * The data accessor is responsible for accessing variable and constant values in an expression. + * The supplier will be called once for each new expression, the default is to create a new {@link + * MapBasedDataAccessor} instance for each expression, providing a new storage for each + * expression. + */ + @Builder.Default + private final Supplier dataAccessorSupplier = MapBasedDataAccessor::new; + + /** + * Default constants will be added automatically to each expression and can be used in expression + * evaluation. + */ + @Builder.Default + private final Map defaultConstants = getStandardConstants(); + + /** + * Support for arrays in expressions are allowed or not. + */ + @Builder.Default private final boolean arraysAllowed = true; + + /** + * Support for structures in expressions are allowed or not. + */ + @Builder.Default private final boolean structuresAllowed = true; + + /** + * Support for the binary (undefined) data type is allowed or not. + * + * @since 3.3.0 + */ + @Builder.Default private final boolean binaryAllowed = false; + + /** + * Support for implicit multiplication, like in (a+b)(b+c) are allowed or not. + */ + @Builder.Default private final boolean implicitMultiplicationAllowed = true; + + /** + * Support for single quote string literals, like in 'Hello World' are allowed or not. + */ + @Builder.Default private final boolean singleQuoteStringLiteralsAllowed = false; + + /** + * Allow for expressions to evaluate without errors when variables are not defined. + * + * @since 3.6.0 + */ + @Builder.Default private final boolean lenientMode = false; + + /** + * The power of operator precedence, can be set higher {@link + * OperatorIfc#OPERATOR_PRECEDENCE_POWER_HIGHER} or to a custom value. + */ + @Builder.Default private final int powerOfPrecedence = OperatorIfc.OPERATOR_PRECEDENCE_POWER; + + /** + * If specified, only the final result of the evaluation will be rounded to the specified number + * of decimal digits, using the MathContexts rounding mode. + * + *

The default value of _DECIMAL_PLACES_ROUNDING_UNLIMITED_ will disable rounding. + */ + @Builder.Default private final int decimalPlacesResult = DECIMAL_PLACES_ROUNDING_UNLIMITED; + + /** + * If specified, all results from operations and functions will be rounded to the specified number + * of decimal digits, using the MathContexts rounding mode. + * + *

Automatic rounding is disabled by default. When enabled, EvalEx will round all input + * variables, constants, intermediate operation and function results and the final result to the + * specified number of decimal digits, using the current rounding mode. Using a value of + * _DECIMAL_PLACES_ROUNDING_UNLIMITED_ will disable automatic rounding. + */ + @Builder.Default private final int decimalPlacesRounding = DECIMAL_PLACES_ROUNDING_UNLIMITED; + + /** + * If set to true (default), then the trailing decimal zeros in a number result will be stripped. + */ + @Builder.Default private final boolean stripTrailingZeros = true; + + /** + * If set to true (default), then variables can be set that have the name of a constant. In that + * case, the constant value will be removed and a variable value will be set. + */ + @Builder.Default private final boolean allowOverwriteConstants = true; + + /** + * The time zone id. By default, the system default zone ID is used. + */ + @Builder.Default private final ZoneId zoneId = ZoneId.systemDefault(); + + /** + * The locale. By default, the system default locale is used. + */ + @Builder.Default private final Locale locale = Locale.getDefault(); + + /** + * The maximum recursion depth allowed for nested expressions. + */ + @Builder.Default private final int maxRecursionDepth = DEFAULT_MAX_RECURSION_DEPTH; + + /** + * The date-time formatters. When parsing, each format will be tried and the first matching will + * be used. For formatting, only the first will be used. + * + *

By default, the {@link ExpressionConfiguration#DEFAULT_DATE_TIME_FORMATTERS} are used. + */ + @Builder.Default + private final List dateTimeFormatters = DEFAULT_DATE_TIME_FORMATTERS; + + /** + * The converter to use when converting different data types to an {@link EvaluationValue}. + */ + @Builder.Default + private final EvaluationValueConverterIfc evaluationValueConverter = + new DefaultEvaluationValueConverter(); + + /** + * Convenience method to create a default configuration. + * + * @return A configuration with default settings. + */ + public static ExpressionConfiguration defaultConfiguration() { + return ExpressionConfiguration.builder().build(); + } + + private static Map getStandardConstants() { + + Map constants = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); + + constants.put("TRUE", EvaluationValue.TRUE); + constants.put("FALSE", EvaluationValue.FALSE); + constants.put( + "PI", + EvaluationValue.numberValue( + new BigDecimal( + "3.1415926535897932384626433832795028841971693993751058209749445923078164062862089986280348253421170679"))); + constants.put( + "E", + EvaluationValue.numberValue( + new BigDecimal( + "2.71828182845904523536028747135266249775724709369995957496696762772407663"))); + constants.put("NULL", EvaluationValue.NULL_VALUE); + + constants.put( + "DT_FORMAT_ISO_DATE_TIME", + EvaluationValue.stringValue("yyyy-MM-dd'T'HH:mm:ss[.SSS][XXX]['['VV']']")); + constants.put( + "DT_FORMAT_LOCAL_DATE_TIME", EvaluationValue.stringValue("yyyy-MM-dd'T'HH:mm:ss[.SSS]")); + constants.put("DT_FORMAT_LOCAL_DATE", EvaluationValue.stringValue("yyyy-MM-dd")); + + return constants; + } + + /** + * Adds additional operators to this configuration. + * + * @param operators variable number of arguments with a map entry holding the operator name and + * implementation.
+ * Example: + * ExpressionConfiguration.defaultConfiguration() .withAdditionalOperators( + * Pair.of("++", new PrefixPlusPlusOperator()), Pair.of("++", new + * PostfixPlusPlusOperator())); + * + * @return The modified configuration, to allow chaining of methods. + */ + @SafeVarargs + public final ExpressionConfiguration withAdditionalOperators( + Map.Entry... operators) { + Arrays.stream(operators) + .forEach(entry -> operatorDictionary.addOperator(entry.getKey(), entry.getValue())); + return this; + } + + /** + * Adds additional functions to this configuration. + * + * @param functions variable number of arguments with a map entry holding the functions name and + * implementation.
+ * Example: + * ExpressionConfiguration.defaultConfiguration() .withAdditionalFunctions( + * Pair.of("save", new SaveFunction()), Pair.of("update", new + * UpdateFunction())); + * + * @return The modified configuration, to allow chaining of methods. + */ + @SafeVarargs + public final ExpressionConfiguration withAdditionalFunctions( + Map.Entry... functions) { + Arrays.stream(functions) + .forEach(entry -> functionDictionary.addFunction(entry.getKey(), entry.getValue())); + return this; + } +} diff --git a/src/main/java/com/ezylang/evalex/config/FunctionDictionaryIfc.java b/src/main/java/com/ezylang/evalex/config/FunctionDictionaryIfc.java new file mode 100644 index 000000000..9ce11bb1c --- /dev/null +++ b/src/main/java/com/ezylang/evalex/config/FunctionDictionaryIfc.java @@ -0,0 +1,76 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.config; + +import com.ezylang.evalex.functions.FunctionIfc; + +import java.util.Set; + +/** + * A function dictionary holds all the functions, that can be used in an expression.
+ * The default implementation is the {@link MapBasedFunctionDictionary}. + */ +public interface FunctionDictionaryIfc { + + /** + * Allows to add a function to the dictionary. Implementation is optional, if you have a fixed set + * of functions, this method can throw an exception. + * + * @param functionName The function name. + * @param function The function implementation. + */ + void addFunction(String functionName, FunctionIfc function); + + /** + * Check if the dictionary has a function with that name. + * + * @param functionName The function name to look for. + * @return true if a function was found or false if not. + */ + default boolean hasFunction(String functionName) { + return getFunction(functionName) != null; + } + + /** + * Get the function definition for a function name. + * + * @param functionName The name of the function. + * @return The function definition or null if no function was found. + */ + FunctionIfc getFunction(String functionName); + + /** + * Get all function names in current configuration. + * + * @return A set of all defined function names. + * @throws UnsupportedOperationException when this operation is not supported by the + * implementation. + */ + default Set getAvailableFunctionNames() { + throw new UnsupportedOperationException("Operation not supported"); + } + + /** + * Get all functions in current configuration. + * + * @return A set of all defined functions. + * @throws UnsupportedOperationException when this operation is not supported by the + * implementation. + */ + default Set getAvailableFunctions() { + throw new UnsupportedOperationException("Operation not supported"); + } +} diff --git a/src/main/java/com/ezylang/evalex/config/MapBasedFunctionDictionary.java b/src/main/java/com/ezylang/evalex/config/MapBasedFunctionDictionary.java new file mode 100644 index 000000000..26278a514 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/config/MapBasedFunctionDictionary.java @@ -0,0 +1,69 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.config; + +import com.ezylang.evalex.functions.FunctionIfc; +import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet; + +import java.util.Collections; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; + +import static java.util.Arrays.stream; + +/** + * A default case-insensitive implementation of the function dictionary that uses a local + * Map.Entry<String, FunctionIfc> for storage. + */ +public class MapBasedFunctionDictionary implements FunctionDictionaryIfc { + + private final Map functions = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); + + /** + * Creates a new function dictionary with the specified list of functions. + * + * @param functions variable number of arguments that specify the function names and definitions + * that will initially be added. + * @return A newly created function dictionary with the specified functions. + */ + @SuppressWarnings({"unchecked", "varargs"}) + public static FunctionDictionaryIfc ofFunctions(Map.Entry... functions) { + FunctionDictionaryIfc dictionary = new MapBasedFunctionDictionary(); + stream(functions).forEach(entry -> dictionary.addFunction(entry.getKey(), entry.getValue())); + return dictionary; + } + + @Override + public FunctionIfc getFunction(String functionName) { + return functions.get(functionName); + } + + @Override + public Set getAvailableFunctionNames() { + return Collections.unmodifiableSet(functions.keySet()); + } + + @Override + public Set getAvailableFunctions() { + return new ObjectOpenHashSet<>(functions.values()); + } + + @Override + public void addFunction(String functionName, FunctionIfc function) { + functions.put(functionName, function); + } +} diff --git a/src/main/java/com/ezylang/evalex/config/MapBasedOperatorDictionary.java b/src/main/java/com/ezylang/evalex/config/MapBasedOperatorDictionary.java new file mode 100644 index 000000000..0a3b863a1 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/config/MapBasedOperatorDictionary.java @@ -0,0 +1,109 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.config; + +import com.ezylang.evalex.operators.OperatorIfc; +import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet; + +import java.util.Collections; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; + +import static java.util.Arrays.stream; + +/** + * A default case-insensitive implementation of the operator dictionary that uses a local + * Map.Entry<String,OperatorIfc> for storage. + */ +public class MapBasedOperatorDictionary implements OperatorDictionaryIfc { + + final Map prefixOperators = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); + + final Map postfixOperators = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); + + final Map infixOperators = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); + + /** + * Creates a new operator dictionary with the specified list of operators. + * + * @param operators variable number of arguments that specify the operator names and definitions + * that will initially be added. + * @return A newly created operator dictionary with the specified operators. + */ + @SuppressWarnings({"unchecked", "varargs"}) + public static OperatorDictionaryIfc ofOperators(Map.Entry... operators) { + OperatorDictionaryIfc dictionary = new MapBasedOperatorDictionary(); + stream(operators).forEach(entry -> dictionary.addOperator(entry.getKey(), entry.getValue())); + return dictionary; + } + + @Override + public void addOperator(String operatorString, OperatorIfc operator) { + if (operator.isPrefix()) { + prefixOperators.put(operatorString, operator); + } else if (operator.isPostfix()) { + postfixOperators.put(operatorString, operator); + } else { + infixOperators.put(operatorString, operator); + } + } + + @Override + public OperatorIfc getPrefixOperator(String operatorString) { + return prefixOperators.get(operatorString); + } + + @Override + public OperatorIfc getPostfixOperator(String operatorString) { + return postfixOperators.get(operatorString); + } + + @Override + public OperatorIfc getInfixOperator(String operatorString) { + return infixOperators.get(operatorString); + } + + @Override + public Set getAvailablePrefixOperatorNames() { + return Collections.unmodifiableSet(prefixOperators.keySet()); + } + + @Override + public Set getAvailablePostfixOperatorNames() { + return Collections.unmodifiableSet(postfixOperators.keySet()); + } + + @Override + public Set getAvailableInfixOperatorNames() { + return Collections.unmodifiableSet(infixOperators.keySet()); + } + + @Override + public Set getAvailablePrefixOperators() { + return new ObjectOpenHashSet<>(prefixOperators.values()); + } + + @Override + public Set getAvailablePostfixOperators() { + return new ObjectOpenHashSet<>(postfixOperators.values()); + } + + @Override + public Set getAvailableInfixOperators() { + return new ObjectOpenHashSet<>(infixOperators.values()); + } +} diff --git a/src/main/java/com/ezylang/evalex/config/OperatorDictionaryIfc.java b/src/main/java/com/ezylang/evalex/config/OperatorDictionaryIfc.java new file mode 100644 index 000000000..9fde252d4 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/config/OperatorDictionaryIfc.java @@ -0,0 +1,156 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.config; + +import com.ezylang.evalex.operators.OperatorIfc; + +import java.util.Set; + +/** + * An operator dictionary holds all the operators, that can be used in an expression.
+ * The default implementation is the {@link MapBasedOperatorDictionary}. + */ +public interface OperatorDictionaryIfc { + + /** + * Allows to add an operator to the dictionary. Implementation is optional, if you have a fixed + * set of operators, this method can throw an exception. + * + * @param operatorString The operator name. + * @param operator The operator implementation. + */ + void addOperator(String operatorString, OperatorIfc operator); + + /** + * Check if the dictionary has a prefix operator with that name. + * + * @param operatorString The operator name to look for. + * @return true if an operator was found or false if not. + */ + default boolean hasPrefixOperator(String operatorString) { + return getPrefixOperator(operatorString) != null; + } + + /** + * Check if the dictionary has a postfix operator with that name. + * + * @param operatorString The operator name to look for. + * @return true if an operator was found or false if not. + */ + default boolean hasPostfixOperator(String operatorString) { + return getPostfixOperator(operatorString) != null; + } + + /** + * Check if the dictionary has an infix operator with that name. + * + * @param operatorString The operator name to look for. + * @return true if an operator was found or false if not. + */ + default boolean hasInfixOperator(String operatorString) { + return getInfixOperator(operatorString) != null; + } + + /** + * Get the operator definition for a prefix operator name. + * + * @param operatorString The name of the operator. + * @return The operator definition or null if no operator was found. + */ + OperatorIfc getPrefixOperator(String operatorString); + + /** + * Get the operator definition for a postfix operator name. + * + * @param operatorString The name of the operator. + * @return The operator definition or null if no operator was found. + */ + OperatorIfc getPostfixOperator(String operatorString); + + /** + * Get the operator definition for an infix operator name. + * + * @param operatorString The name of the operator. + * @return The operator definition or null if no operator was found. + */ + OperatorIfc getInfixOperator(String operatorString); + + /** + * Get all prefix operator names in current configuration. + * + * @return A set of all defined prefix operator names. + * @throws UnsupportedOperationException when this operation is not supported by the + * implementation. + */ + default Set getAvailablePrefixOperatorNames() { + throw new UnsupportedOperationException("Operation not supported"); + } + + /** + * Get all postfix operator names in current configuration. + * + * @return A set of all defined postfix operator names. + * @throws UnsupportedOperationException when this operation is not supported by the + * implementation. + */ + default Set getAvailablePostfixOperatorNames() { + throw new UnsupportedOperationException("Operation not supported"); + } + + /** + * Get all infix operator names in current configuration. + * + * @return A set of all defined infix operator names. + * @throws UnsupportedOperationException when this operation is not supported by the + * implementation. + */ + default Set getAvailableInfixOperatorNames() { + throw new UnsupportedOperationException("Operation not supported"); + } + + /** + * Get all prefix operators in current configuration. + * + * @return A set of all defined prefix operators. + * @throws UnsupportedOperationException when this operation is not supported by the + * implementation. + */ + default Set getAvailablePrefixOperators() { + throw new UnsupportedOperationException("Operation not supported"); + } + + /** + * Get all postfix operators in current configuration. + * + * @return A set of all defined postfix operators. + * @throws UnsupportedOperationException when this operation is not supported by the + * implementation. + */ + default Set getAvailablePostfixOperators() { + throw new UnsupportedOperationException("Operation not supported"); + } + + /** + * Get all infix operators in current configuration. + * + * @return A set of all defined infix operators. + * @throws UnsupportedOperationException when this operation is not supported by the + * implementation. + */ + default Set getAvailableInfixOperators() { + throw new UnsupportedOperationException("Operation not supported"); + } +} diff --git a/src/main/java/com/ezylang/evalex/data/DataAccessorIfc.java b/src/main/java/com/ezylang/evalex/data/DataAccessorIfc.java new file mode 100644 index 000000000..434c278f3 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/data/DataAccessorIfc.java @@ -0,0 +1,40 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.data; + +/** + * A data accessor is responsible for accessing data, e.g. variable and constant values during an + * expression evaluation. The default implementation for setting and reading local data is the + * {@link MapBasedDataAccessor}. + */ +public interface DataAccessorIfc { + + /** + * Retrieves a data value. + * + * @param variable The variable name, e.g. a variable or constant name. + * @return The data value, or null if not found. + */ + EvaluationValue getData(String variable); + + /** + * Sets a data value. + * + * @param variable The variable name, e.g. a variable or constant name. + * @param value The value to set. + */ + void setData(String variable, EvaluationValue value); +} diff --git a/src/main/java/com/ezylang/evalex/data/EvaluationValue.java b/src/main/java/com/ezylang/evalex/data/EvaluationValue.java new file mode 100644 index 000000000..594676e86 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/data/EvaluationValue.java @@ -0,0 +1,576 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.data; + +import com.ezylang.evalex.config.ExpressionConfiguration; +import com.ezylang.evalex.parser.ASTNode; +import lombok.Value; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.math.MathContext; +import java.time.DateTimeException; +import java.time.Duration; +import java.time.Instant; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +/** + * The representation of the final or intermediate evaluation result value. The representation + * consists of a data type and data value. Depending on the type, the value will be stored in a + * corresponding object type. + */ +@Value +public class EvaluationValue implements Comparable { + + /** + * A pre-built, immutable, null value. + */ + public static final EvaluationValue NULL_VALUE = new EvaluationValue(null, DataType.NULL); + + /** + * A pre-built, immutable value for an undefined variable, when the lenient mode is enabled. + * + * @since 3.6.0 + */ + public static final EvaluationValue UNDEFINED = new EvaluationValue(null, DataType.UNDEFINED); + + /** + * A pre-built, immutable, false boolean value. + */ + public static final EvaluationValue FALSE = new EvaluationValue(false, DataType.BOOLEAN); + + /** + * A pre-built, immutable, true boolean value. + */ + public static final EvaluationValue TRUE = new EvaluationValue(true, DataType.BOOLEAN); + + /** + * Return value for a null {@link DataType#BOOLEAN}. + */ + private static final Boolean NULL_BOOLEAN = null; + + /** + * Return value for a null {@link DataType#ARRAY}. + */ + private static final List NULL_ARRAY = null; + + /** + * Return value for a null {@link DataType#STRUCTURE}. + */ + private static final Map NULL_STRUCTURE = null; + + /** + * The supported data types. + */ + public enum DataType { + /** + * A string of characters, stored as {@link String}. + */ + STRING, + /** + * Any number, stored as {@link BigDecimal}. + */ + NUMBER, + /** + * A boolean, stored as {@link Boolean}. + */ + BOOLEAN, + /** + * A date time value, stored as {@link java.time.Instant}. + */ + DATE_TIME, + /** + * A period value, stored as {@link java.time.Duration}. + */ + DURATION, + /** + * A list evaluation values. Stored as {@link java.util.List}. + */ + ARRAY, + /** + * A structure with pairs of name/value members. Name is a string and the value is a {@link + * EvaluationValue}. Stored as a {@link java.util.Map}. + */ + STRUCTURE, + /** + * Used for lazy parameter evaluation, stored as an {@link ASTNode}, which can be evaluated on + * demand. + */ + EXPRESSION_NODE, + /** + * A null value + */ + NULL, + /** + * Raw (undefined) type, stored as an {@link Object}. + */ + BINARY, + /** + * Applicable for undeclared variables when lenient mode is enabled. + * + * @since 3.6.0 + */ + UNDEFINED + } + + Object value; + + DataType dataType; + + /** + * Creates a new evaluation value by using the configured converter and configuration. + * + * @param value One of the supported data types. + * @param configuration The expression configuration to use. + * @throws IllegalArgumentException if the data type can't be mapped. + * @see ExpressionConfiguration#getEvaluationValueConverter() + * @deprecated Use {@link EvaluationValue#of(Object, ExpressionConfiguration)} instead. + */ + @Deprecated + public EvaluationValue(Object value, ExpressionConfiguration configuration) { + + EvaluationValue converted = + configuration.getEvaluationValueConverter().convertObject(value, configuration); + + this.value = converted.getValue(); + this.dataType = converted.getDataType(); + } + + /** + * Private constructor to directly create an instance with a given type and value. + * + * @param value The value to set, no conversion will be done. + * @param dataType The data type to set. + */ + private EvaluationValue(Object value, DataType dataType) { + this.dataType = dataType; + this.value = value; + } + + /** + * Creates a new evaluation value by using the configured converter and configuration. + * + * @param value One of the supported data types. + * @param configuration The expression configuration to use; not null + * @throws IllegalArgumentException if the data type can't be mapped. + * @see ExpressionConfiguration#getEvaluationValueConverter() + */ + public static EvaluationValue of(Object value, ExpressionConfiguration configuration) { + return configuration.getEvaluationValueConverter().convertObject(value, configuration); + } + + /** + * Returns an immutable null value. + * + * @return A null value. + * @deprecated Use {@link EvaluationValue#NULL_VALUE} instead + */ + @Deprecated + public static EvaluationValue nullValue() { + return NULL_VALUE; + } + + /** + * Creates a new number value. + * + * @param value The BigDecimal value to use. + * @return the new number value. + */ + public static EvaluationValue numberValue(BigDecimal value) { + return new EvaluationValue(value, DataType.NUMBER); + } + + /** + * Creates a new string value. + * + * @param value The String value to use. + * @return the new string value. + */ + public static EvaluationValue stringValue(String value) { + return new EvaluationValue(value, DataType.STRING); + } + + /** + * Creates a new boolean value. + * + * @param value The Boolean value to use. + * @return the new boolean value. + */ + public static EvaluationValue booleanValue(Boolean value) { + return value != null && value ? TRUE : FALSE; + } + + /** + * Creates a new date-time value. + * + * @param value The Instant value to use. + * @return the new date-time value. + */ + public static EvaluationValue dateTimeValue(Instant value) { + return new EvaluationValue(value, DataType.DATE_TIME); + } + + /** + * Creates a new duration value. + * + * @param value The Duration value to use. + * @return the new duration value. + */ + public static EvaluationValue durationValue(Duration value) { + return new EvaluationValue(value, DataType.DURATION); + } + + /** + * Creates a new expression node value. + * + * @param value The ASTNode value to use. + * @return the new expression node value. + */ + public static EvaluationValue expressionNodeValue(ASTNode value) { + return new EvaluationValue(value, DataType.EXPRESSION_NODE); + } + + /** + * Creates a new array value. + * + * @param value The List value to use. + * @return the new array value. + */ + public static EvaluationValue arrayValue(List value) { + return new EvaluationValue(value, DataType.ARRAY); + } + + /** + * Creates a new structure value. + * + * @param value The Map value to use. + * @return the new structure value. + */ + public static EvaluationValue structureValue(Map value) { + return new EvaluationValue(value, DataType.STRUCTURE); + } + + /** + * Creates a new binary (raw) value. + * + * @param value The Object to use. + * @return the new binary value. + * @since 3.3.0 + */ + public static EvaluationValue binaryValue(Object value) { + return new EvaluationValue(value, DataType.BINARY); + } + + /** + * Checks if the value is of type {@link DataType#NUMBER}. + * + * @return true or false. + */ + public boolean isNumberValue() { + return getDataType() == DataType.NUMBER; + } + + /** + * Checks if the value is of type {@link DataType#STRING}. + * + * @return true or false. + */ + public boolean isStringValue() { + return getDataType() == DataType.STRING; + } + + /** + * Checks if the value is of type {@link DataType#BOOLEAN}. + * + * @return true or false. + */ + public boolean isBooleanValue() { + return getDataType() == DataType.BOOLEAN; + } + + /** + * Checks if the value is of type {@link DataType#DATE_TIME}. + * + * @return true or false. + */ + public boolean isDateTimeValue() { + return getDataType() == DataType.DATE_TIME; + } + + /** + * Checks if the value is of type {@link DataType#DURATION}. + * + * @return true or false. + */ + public boolean isDurationValue() { + return getDataType() == DataType.DURATION; + } + + /** + * Checks if the value is of type {@link DataType#ARRAY}. + * + * @return true or false. + */ + public boolean isArrayValue() { + return getDataType() == DataType.ARRAY; + } + + /** + * Checks if the value is of type {@link DataType#STRUCTURE}. + * + * @return true or false. + */ + public boolean isStructureValue() { + return getDataType() == DataType.STRUCTURE; + } + + /** + * Checks if the value is of type {@link DataType#EXPRESSION_NODE}. + * + * @return true or false. + */ + public boolean isExpressionNode() { + return getDataType() == DataType.EXPRESSION_NODE; + } + + public boolean isNullValue() { + return getDataType() == DataType.NULL; + } + + /** + * Checks if the value is of type {@link DataType#BINARY}. + * + * @return true or false. + * @since 3.3.0 + */ + public boolean isBinaryValue() { + return getDataType() == DataType.BINARY; + } + + /** + * Creates a {@link DataType#NUMBER} value from a {@link String}. + * + * @param value The {@link String} value. + * @param mathContext The math context to use for creation of the {@link BigDecimal} storage. + */ + public static EvaluationValue numberOfString(String value, MathContext mathContext) { + if (value.startsWith("0x") || value.startsWith("0X")) { + BigInteger hexToInteger = new BigInteger(value.substring(2), 16); + return EvaluationValue.numberValue(new BigDecimal(hexToInteger, mathContext)); + } else { + return EvaluationValue.numberValue(new BigDecimal(value, mathContext)); + } + } + + /** + * Gets a {@link BigDecimal} representation of the value. If possible and needed, a conversion + * will be made. + * + *

    + *
  • Boolean true will return a {@link BigDecimal#ONE}, else {@link + * BigDecimal#ZERO}. + *
+ * + * @return The {@link BigDecimal} representation of the value, or {@link BigDecimal#ZERO} if + * conversion is not possible. + */ + public BigDecimal getNumberValue() { + switch (getDataType()) { + case NUMBER: + return (BigDecimal) value; + case BOOLEAN: + return (Boolean.TRUE.equals(value) ? BigDecimal.ONE : BigDecimal.ZERO); + case STRING: + return Boolean.parseBoolean((String) value) ? BigDecimal.ONE : BigDecimal.ZERO; + case NULL: + return null; + default: + return BigDecimal.ZERO; + } + } + + /** + * Gets a {@link String} representation of the value. If possible and needed, a conversion will be + * made. + * + *
    + *
  • Number values will be returned as {@link BigDecimal#toPlainString()}. + *
  • The {@link Object#toString()} will be used in all other cases. + *
+ * + * @return The {@link String} representation of the value. + */ + public String getStringValue() { + switch (getDataType()) { + case NUMBER: + return ((BigDecimal) value).toPlainString(); + case NULL: + return null; + default: + return value.toString(); + } + } + + /** + * Gets a {@link Boolean} representation of the value. If possible and needed, a conversion will + * be made. + * + *
    + *
  • Any non-zero number value will return true. + *
  • Any string with the value "true" (case ignored) will return true. + *
+ * + * @return The {@link Boolean} representation of the value. + */ + public Boolean getBooleanValue() { + switch (getDataType()) { + case NUMBER: + return getNumberValue().compareTo(BigDecimal.ZERO) != 0; + case BOOLEAN: + return (Boolean) value; + case STRING: + return Boolean.parseBoolean((String) value); + case NULL: + return NULL_BOOLEAN; + default: + return Boolean.FALSE; + } + } + + /** + * Gets a {@link Instant} representation of the value. If possible and needed, a conversion will + * be made. + * + *
    + *
  • Any number value will return the instant from the epoc value. + *
  • Any string with the string representation of a LocalDateTime (ex: + * "2018-11-30T18:35:24.00") (case ignored) will return the current LocalDateTime. + *
  • The date {@link Instant#EPOCH} will return if a conversion error occurs or in all other + * cases. + *
+ * + * @return The {@link Instant} representation of the value. + */ + public Instant getDateTimeValue() { + try { + switch (getDataType()) { + case NUMBER: + return Instant.ofEpochMilli(((BigDecimal) value).longValue()); + case DATE_TIME: + return (Instant) value; + case STRING: + return Instant.parse((String) value); + default: + return Instant.EPOCH; + } + } catch (DateTimeException ex) { + return Instant.EPOCH; + } + } + + /** + * Gets a {@link Duration} representation of the value. If possible and needed, a conversion will + * be made. + * + *
    + *
  • Any non-zero number value will return the duration from the millisecond. + *
  • Any string with the string representation of an {@link Duration} (ex: + * "PnDTnHnMn.nS") (case ignored) will return the current instant. + *
  • The {@link Duration#ZERO} will return if a conversion error occurs or in all other cases. + *
+ * + * @return The {@link Duration} representation of the value. + */ + public Duration getDurationValue() { + try { + switch (getDataType()) { + case NUMBER: + return Duration.ofMillis(((BigDecimal) value).longValue()); + case DURATION: + return (Duration) value; + case STRING: + return Duration.parse((String) value); + default: + return Duration.ZERO; + } + } catch (DateTimeException ex) { + return Duration.ZERO; + } + } + + /** + * Gets a {@link List} representation of the value. + * + * @return The {@link List} representation of the value or an empty list, if no + * conversion is possible. + */ + @SuppressWarnings("unchecked") + public List getArrayValue() { + if (isArrayValue()) { + return (List) value; + } else if (isNullValue()) { + return NULL_ARRAY; + } else { + return Collections.emptyList(); + } + } + + /** + * Gets a {@link Map} representation of the value. + * + * @return The {@link Map} representation of the value or an empty list, if no conversion is + * possible. + */ + @SuppressWarnings("unchecked") + public Map getStructureValue() { + if (isStructureValue()) { + return (Map) value; + } else if (isNullValue()) { + return NULL_STRUCTURE; + } else { + return Collections.emptyMap(); + } + } + + /** + * Gets the expression node, if this value is of type {@link DataType#EXPRESSION_NODE}. + * + * @return The expression node, or null for any other data type. + */ + public ASTNode getExpressionNode() { + return isExpressionNode() ? ((ASTNode) getValue()) : null; + } + + @Override + public int compareTo(EvaluationValue toCompare) { + switch (getDataType()) { + case NUMBER: + return getNumberValue().compareTo(toCompare.getNumberValue()); + case BOOLEAN: + return getBooleanValue().compareTo(toCompare.getBooleanValue()); + case NULL: + throw new NullPointerException("Can not compare a null value"); + case DATE_TIME: + return getDateTimeValue().compareTo(toCompare.getDateTimeValue()); + case DURATION: + return getDurationValue().compareTo(toCompare.getDurationValue()); + default: + return getStringValue().compareTo(toCompare.getStringValue()); + } + } +} diff --git a/src/main/java/com/ezylang/evalex/data/MapBasedDataAccessor.java b/src/main/java/com/ezylang/evalex/data/MapBasedDataAccessor.java new file mode 100644 index 000000000..b38a36052 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/data/MapBasedDataAccessor.java @@ -0,0 +1,39 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.data; + +import java.util.Map; +import java.util.TreeMap; + +/** + * A default case-insensitive implementation of the data accessor that uses a local + * Map.Entry<String, EvaluationValue> for storage. + */ +public class MapBasedDataAccessor implements DataAccessorIfc { + + private final Map variables = + new TreeMap<>(String.CASE_INSENSITIVE_ORDER); + + @Override + public EvaluationValue getData(String variable) { + return variables.get(variable); + } + + @Override + public void setData(String variable, EvaluationValue value) { + variables.put(variable, value); + } +} diff --git a/src/main/java/com/ezylang/evalex/data/conversion/ArrayConverter.java b/src/main/java/com/ezylang/evalex/data/conversion/ArrayConverter.java new file mode 100644 index 000000000..58a315762 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/data/conversion/ArrayConverter.java @@ -0,0 +1,159 @@ +/* + Copyright 2012-2023 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.data.conversion; + +import com.ezylang.evalex.config.ExpressionConfiguration; +import com.ezylang.evalex.data.EvaluationValue; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +/** + * Converter to convert to the ARRAY data type. + */ +public class ArrayConverter implements ConverterIfc { + + @Override + public EvaluationValue convert(Object object, ExpressionConfiguration configuration) { + List list; + + if (object.getClass().isArray()) { + list = convertArray(object, configuration); + } else if (object instanceof List) { + list = convertList((List) object, configuration); + } else { + throw illegalArgument(object); + } + + return EvaluationValue.arrayValue(list); + } + + @Override + public boolean canConvert(Object object) { + return object instanceof List || object.getClass().isArray(); + } + + private static List convertList( + List object, ExpressionConfiguration configuration) { + return object.stream() + .map(element -> EvaluationValue.of(element, configuration)) + .collect(Collectors.toList()); + } + + private List convertArray(Object array, ExpressionConfiguration configuration) { + if (array instanceof int[]) { + return convertIntArray((int[]) array, configuration); + } else if (array instanceof long[]) { + return convertLongArray((long[]) array, configuration); + } else if (array instanceof double[]) { + return convertDoubleArray((double[]) array, configuration); + } else if (array instanceof float[]) { + return convertFloatArray((float[]) array, configuration); + } else if (array instanceof short[]) { + return convertShortArray((short[]) array, configuration); + } else if (array instanceof char[]) { + return convertCharArray((char[]) array, configuration); + } else if (array instanceof byte[]) { + return convertByteArray((byte[]) array, configuration); + } else if (array instanceof boolean[]) { + return convertBooleanArray((boolean[]) array, configuration); + } else { + return convertObjectArray((Object[]) array, configuration); + } + } + + private List convertIntArray( + int[] array, ExpressionConfiguration configuration) { + List list = new ArrayList<>(); + for (int i : array) { + list.add(EvaluationValue.of(i, configuration)); + } + return list; + } + + private List convertLongArray( + long[] array, ExpressionConfiguration configuration) { + List list = new ArrayList<>(); + for (long l : array) { + list.add(EvaluationValue.of(l, configuration)); + } + return list; + } + + private List convertDoubleArray( + double[] array, ExpressionConfiguration configuration) { + List list = new ArrayList<>(); + for (double d : array) { + list.add(EvaluationValue.of(d, configuration)); + } + return list; + } + + private List convertFloatArray( + float[] array, ExpressionConfiguration configuration) { + List list = new ArrayList<>(); + for (float f : array) { + list.add(EvaluationValue.of(f, configuration)); + } + return list; + } + + private List convertShortArray( + short[] array, ExpressionConfiguration configuration) { + List list = new ArrayList<>(); + for (short s : array) { + list.add(EvaluationValue.of(s, configuration)); + } + return list; + } + + private List convertCharArray( + char[] array, ExpressionConfiguration configuration) { + List list = new ArrayList<>(); + for (char c : array) { + list.add(EvaluationValue.of(c, configuration)); + } + return list; + } + + private List convertByteArray( + byte[] array, ExpressionConfiguration configuration) { + List list = new ArrayList<>(); + for (byte b : array) { + list.add(EvaluationValue.of(b, configuration)); + } + return list; + } + + private List convertBooleanArray( + boolean[] array, ExpressionConfiguration configuration) { + List list = new ArrayList<>(); + for (boolean b : array) { + list.add(EvaluationValue.of(b, configuration)); + } + return list; + } + + private List convertObjectArray( + Object[] array, ExpressionConfiguration configuration) { + List list = new ArrayList<>(); + for (Object o : array) { + list.add(EvaluationValue.of(o, configuration)); + } + return list; + } +} diff --git a/src/main/java/com/ezylang/evalex/data/conversion/BinaryConverter.java b/src/main/java/com/ezylang/evalex/data/conversion/BinaryConverter.java new file mode 100644 index 000000000..8af35f4c2 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/data/conversion/BinaryConverter.java @@ -0,0 +1,38 @@ +/* + Copyright 2012-2024 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.data.conversion; + +import com.ezylang.evalex.config.ExpressionConfiguration; +import com.ezylang.evalex.data.EvaluationValue; + +/** + * Converter to convert to the BINARY data type. + * + * @author oswaldobapvicjr + * @since 3.3.0 + */ +public class BinaryConverter implements ConverterIfc { + + @Override + public EvaluationValue convert(Object object, ExpressionConfiguration configuration) { + return EvaluationValue.binaryValue(object); + } + + @Override + public boolean canConvert(Object object) { + return true; + } +} diff --git a/src/main/java/com/ezylang/evalex/data/conversion/BooleanConverter.java b/src/main/java/com/ezylang/evalex/data/conversion/BooleanConverter.java new file mode 100644 index 000000000..ebfb3b2e4 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/data/conversion/BooleanConverter.java @@ -0,0 +1,35 @@ +/* + Copyright 2012-2023 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.data.conversion; + +import com.ezylang.evalex.config.ExpressionConfiguration; +import com.ezylang.evalex.data.EvaluationValue; + +/** + * Converter to convert to the BOOLEAN data type. + */ +public class BooleanConverter implements ConverterIfc { + + @Override + public EvaluationValue convert(Object object, ExpressionConfiguration configuration) { + return EvaluationValue.booleanValue((Boolean) object); + } + + @Override + public boolean canConvert(Object object) { + return object instanceof Boolean; + } +} diff --git a/src/main/java/com/ezylang/evalex/data/conversion/ConverterIfc.java b/src/main/java/com/ezylang/evalex/data/conversion/ConverterIfc.java new file mode 100644 index 000000000..ee1b9f44c --- /dev/null +++ b/src/main/java/com/ezylang/evalex/data/conversion/ConverterIfc.java @@ -0,0 +1,47 @@ +/* + Copyright 2012-2023 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.data.conversion; + +import com.ezylang.evalex.config.ExpressionConfiguration; +import com.ezylang.evalex.data.EvaluationValue; + +/** + * Converter interface used by the {@link DefaultEvaluationValueConverter}. + */ +public interface ConverterIfc { + + /** + * Called to convert a previously checked data type. + * + * @param object The object to convert. + * @param configuration The current expression configuration. + * @return The converted value. + */ + EvaluationValue convert(Object object, ExpressionConfiguration configuration); + + /** + * Checks, if a given object can be converted by this converter. + * + * @param object The object to convert. + * @return true if the object can be converted, false otherwise. + */ + boolean canConvert(Object object); + + default IllegalArgumentException illegalArgument(Object object) { + return new IllegalArgumentException( + "Unsupported data type '" + object.getClass().getName() + "'"); + } +} diff --git a/src/main/java/com/ezylang/evalex/data/conversion/DateTimeConverter.java b/src/main/java/com/ezylang/evalex/data/conversion/DateTimeConverter.java new file mode 100644 index 000000000..2474273d9 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/data/conversion/DateTimeConverter.java @@ -0,0 +1,111 @@ +/* + Copyright 2012-2023 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.data.conversion; + +import com.ezylang.evalex.config.ExpressionConfiguration; +import com.ezylang.evalex.data.EvaluationValue; + +import java.time.DateTimeException; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.OffsetDateTime; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.time.temporal.TemporalAccessor; +import java.time.temporal.TemporalQueries; +import java.util.Calendar; +import java.util.Date; +import java.util.List; + +/** + * Converter to convert to the DATE_TIME data type. + */ +public class DateTimeConverter implements ConverterIfc { + + @Override + public EvaluationValue convert(Object object, ExpressionConfiguration configuration) { + + Instant instant; + + if (object instanceof Instant) { + instant = (Instant) object; + } else if (object instanceof ZonedDateTime) { + instant = ((ZonedDateTime) object).toInstant(); + } else if (object instanceof OffsetDateTime) { + instant = ((OffsetDateTime) object).toInstant(); + } else if (object instanceof LocalDate) { + instant = ((LocalDate) object).atStartOfDay().atZone(configuration.getZoneId()).toInstant(); + } else if (object instanceof LocalDateTime) { + instant = ((LocalDateTime) object).atZone(configuration.getZoneId()).toInstant(); + } else if (object instanceof Date) { + instant = ((Date) object).toInstant(); + } else if (object instanceof Calendar) { + instant = ((Calendar) object).toInstant(); + } else { + throw illegalArgument(object); + } + return EvaluationValue.dateTimeValue(instant); + } + + /** + * Tries to parse a date-time string by trying out each format in the list. The first matching + * result is returned. If none of the formats can be used to parse the string, null + * is returned. + * + * @param value The string to parse. + * @param zoneId The {@link ZoneId} to use for parsing. + * @param formatters The list of formatters. + * @return A parsed {@link Instant} if parsing was successful, else null. + */ + public Instant parseDateTime(String value, ZoneId zoneId, List formatters) { + for (DateTimeFormatter formatter : formatters) { + try { + return parseToInstant(value, zoneId, formatter); + } catch (DateTimeException ignored) { + // ignore + } + } + return null; + } + + private Instant parseToInstant(String value, ZoneId zoneId, DateTimeFormatter formatter) { + TemporalAccessor ta = formatter.parse(value); + ZoneId parsedZoneId = ta.query(TemporalQueries.zone()); + if (parsedZoneId == null) { + LocalDate parsedDate = ta.query(TemporalQueries.localDate()); + LocalTime parsedTime = ta.query(TemporalQueries.localTime()); + if (parsedTime == null) { + parsedTime = parsedDate.atStartOfDay().toLocalTime(); + } + ta = ZonedDateTime.of(parsedDate, parsedTime, zoneId); + } + return Instant.from(ta); + } + + @Override + public boolean canConvert(Object object) { + return (object instanceof Instant + || object instanceof ZonedDateTime + || object instanceof OffsetDateTime + || object instanceof LocalDate + || object instanceof LocalDateTime + || object instanceof Date + || object instanceof Calendar); + } +} diff --git a/src/main/java/com/ezylang/evalex/data/conversion/DefaultEvaluationValueConverter.java b/src/main/java/com/ezylang/evalex/data/conversion/DefaultEvaluationValueConverter.java new file mode 100644 index 000000000..69381d3b6 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/data/conversion/DefaultEvaluationValueConverter.java @@ -0,0 +1,94 @@ +/* + Copyright 2012-2023 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.data.conversion; + +import com.ezylang.evalex.config.ExpressionConfiguration; +import com.ezylang.evalex.data.EvaluationValue; + +import java.util.Arrays; +import java.util.List; + +/** + * The default implementation of the {@link EvaluationValueConverterIfc}, used in the standard + * configuration. + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
Input typeConverter used
BigDecimalNumberConverter
Long, longNumberConverter
Integer, intNumberConverter
Short, shortNumberConverter
Byte, byteNumberConverter
Double, doubleNumberConverter *
Float, floatNumberConverter *
CharSequence , StringStringConverter
Boolean, booleanBooleanConverter
InstantDateTimeConverter
DateDateTimeConverter
CalendarDateTimeConverter
ZonedDateTimeDateTimeConverter
LocalDateDateTimeConverter - the configured zone ID will be used for conversion
LocalDateTimeDateTimeConverter - the configured zone ID will be used for conversion
OffsetDateTimeDateTimeConverter
DurationDurationConverter
ASTNodeASTNode
List<?>ArrayConverter - each entry will be converted
Map<?,?>StructureConverter - each entry will be converted
+ * + * * Be careful with conversion problems when using float or double, which are fractional + * numbers. A (float)0.1 is e.g. converted to 0.10000000149011612 + */ +public class DefaultEvaluationValueConverter implements EvaluationValueConverterIfc { + + static final List converters = + Arrays.asList( + new NumberConverter(), + new StringConverter(), + new BooleanConverter(), + new DateTimeConverter(), + new DurationConverter(), + new ExpressionNodeConverter(), + new ArrayConverter(), + new StructureConverter()); + + @Override + public EvaluationValue convertObject(Object object, ExpressionConfiguration configuration) { + + if (object == null) { + return EvaluationValue.NULL_VALUE; + } + + if (object instanceof EvaluationValue) { + return (EvaluationValue) object; + } + + for (ConverterIfc converter : converters) { + if (converter.canConvert(object)) { + return converter.convert(object, configuration); + } + } + + if (configuration.isBinaryAllowed()) { + return EvaluationValue.binaryValue(object); + } + + throw new IllegalArgumentException( + "Unsupported data type '" + object.getClass().getName() + "'"); + } +} diff --git a/src/main/java/com/ezylang/evalex/data/conversion/DurationConverter.java b/src/main/java/com/ezylang/evalex/data/conversion/DurationConverter.java new file mode 100644 index 000000000..f2594947c --- /dev/null +++ b/src/main/java/com/ezylang/evalex/data/conversion/DurationConverter.java @@ -0,0 +1,37 @@ +/* + Copyright 2012-2023 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.data.conversion; + +import com.ezylang.evalex.config.ExpressionConfiguration; +import com.ezylang.evalex.data.EvaluationValue; + +import java.time.Duration; + +/** + * Converter to convert to the DURATION data type. + */ +public class DurationConverter implements ConverterIfc { + + @Override + public EvaluationValue convert(Object object, ExpressionConfiguration configuration) { + return EvaluationValue.durationValue((Duration) object); + } + + @Override + public boolean canConvert(Object object) { + return object instanceof Duration; + } +} diff --git a/src/main/java/com/ezylang/evalex/data/conversion/EvaluationValueConverterIfc.java b/src/main/java/com/ezylang/evalex/data/conversion/EvaluationValueConverterIfc.java new file mode 100644 index 000000000..31daf2f16 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/data/conversion/EvaluationValueConverterIfc.java @@ -0,0 +1,36 @@ +/* + Copyright 2012-2023 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.data.conversion; + +import com.ezylang.evalex.config.ExpressionConfiguration; +import com.ezylang.evalex.data.EvaluationValue; + +/** + * Converter interface to be implemented by configurable evaluation value converters. Converts an + * arbitrary object to an {@link EvaluationValue}, using the specified configuration. + */ +public interface EvaluationValueConverterIfc { + + /** + * Called whenever an object has to be converted to an {@link EvaluationValue}. + * + * @param object The object holding the value. + * @param configuration The configuration to use. + * @return The converted {@link EvaluationValue}. + * @throws IllegalArgumentException if the object can't be converted. + */ + EvaluationValue convertObject(Object object, ExpressionConfiguration configuration); +} diff --git a/src/main/java/com/ezylang/evalex/data/conversion/ExpressionNodeConverter.java b/src/main/java/com/ezylang/evalex/data/conversion/ExpressionNodeConverter.java new file mode 100644 index 000000000..4ebbfb265 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/data/conversion/ExpressionNodeConverter.java @@ -0,0 +1,36 @@ +/* + Copyright 2012-2023 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.data.conversion; + +import com.ezylang.evalex.config.ExpressionConfiguration; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.parser.ASTNode; + +/** + * Converter to convert to the EXPRESSION_NODE data type. + */ +public class ExpressionNodeConverter implements ConverterIfc { + + @Override + public EvaluationValue convert(Object object, ExpressionConfiguration configuration) { + return EvaluationValue.expressionNodeValue((ASTNode) object); + } + + @Override + public boolean canConvert(Object object) { + return object instanceof ASTNode; + } +} diff --git a/src/main/java/com/ezylang/evalex/data/conversion/NumberConverter.java b/src/main/java/com/ezylang/evalex/data/conversion/NumberConverter.java new file mode 100644 index 000000000..4cc77736e --- /dev/null +++ b/src/main/java/com/ezylang/evalex/data/conversion/NumberConverter.java @@ -0,0 +1,67 @@ +/* + Copyright 2012-2023 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.data.conversion; + +import com.ezylang.evalex.config.ExpressionConfiguration; +import com.ezylang.evalex.data.EvaluationValue; + +import java.math.BigDecimal; +import java.math.BigInteger; + +/** + * Converter to convert to the NUMBER data type. + */ +public class NumberConverter implements ConverterIfc { + + @Override + public EvaluationValue convert(Object object, ExpressionConfiguration configuration) { + BigDecimal bigDecimal; + + if (object instanceof BigDecimal) { + bigDecimal = (BigDecimal) object; + } else if (object instanceof BigInteger) { + bigDecimal = new BigDecimal((BigInteger) object, configuration.getMathContext()); + } else if (object instanceof Double) { + bigDecimal = new BigDecimal(Double.toString((double) object), configuration.getMathContext()); + } else if (object instanceof Float) { + bigDecimal = BigDecimal.valueOf((float) object); + } else if (object instanceof Integer) { + bigDecimal = BigDecimal.valueOf((int) object); + } else if (object instanceof Long) { + bigDecimal = BigDecimal.valueOf((long) object); + } else if (object instanceof Short) { + bigDecimal = BigDecimal.valueOf((short) object); + } else if (object instanceof Byte) { + bigDecimal = BigDecimal.valueOf((byte) object); + } else { + throw illegalArgument(object); + } + + return EvaluationValue.numberValue(bigDecimal); + } + + @Override + public boolean canConvert(Object object) { + return (object instanceof BigDecimal + || object instanceof BigInteger + || object instanceof Double + || object instanceof Float + || object instanceof Integer + || object instanceof Long + || object instanceof Short + || object instanceof Byte); + } +} diff --git a/src/main/java/com/ezylang/evalex/data/conversion/StringConverter.java b/src/main/java/com/ezylang/evalex/data/conversion/StringConverter.java new file mode 100644 index 000000000..d90d9c4f5 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/data/conversion/StringConverter.java @@ -0,0 +1,45 @@ +/* + Copyright 2012-2023 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.data.conversion; + +import com.ezylang.evalex.config.ExpressionConfiguration; +import com.ezylang.evalex.data.EvaluationValue; + +/** + * Converter to convert to the STRING data type. + */ +public class StringConverter implements ConverterIfc { + + @Override + public EvaluationValue convert(Object object, ExpressionConfiguration configuration) { + String string; + + if (object instanceof CharSequence) { + string = ((CharSequence) object).toString(); + } else if (object instanceof Character) { + string = ((Character) object).toString(); + } else { + throw illegalArgument(object); + } + + return EvaluationValue.stringValue(string); + } + + @Override + public boolean canConvert(Object object) { + return (object instanceof CharSequence || object instanceof Character); + } +} diff --git a/src/main/java/com/ezylang/evalex/data/conversion/StructureConverter.java b/src/main/java/com/ezylang/evalex/data/conversion/StructureConverter.java new file mode 100644 index 000000000..8c89874b5 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/data/conversion/StructureConverter.java @@ -0,0 +1,43 @@ +/* + Copyright 2012-2023 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.data.conversion; + +import com.ezylang.evalex.config.ExpressionConfiguration; +import com.ezylang.evalex.data.EvaluationValue; + +import java.util.HashMap; +import java.util.Map; + +/** + * Converter to convert to the STRUCTURE data type. + */ +public class StructureConverter implements ConverterIfc { + + @Override + public EvaluationValue convert(Object object, ExpressionConfiguration configuration) { + Map structure = new HashMap<>(); + for (Map.Entry entry : ((Map) object).entrySet()) { + String name = entry.getKey().toString(); + structure.put(name, EvaluationValue.of(entry.getValue(), configuration)); + } + return EvaluationValue.structureValue(structure); + } + + @Override + public boolean canConvert(Object object) { + return object instanceof Map; + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/AbstractFunction.java b/src/main/java/com/ezylang/evalex/functions/AbstractFunction.java new file mode 100644 index 000000000..8f19daa5f --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/AbstractFunction.java @@ -0,0 +1,103 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.functions; + +import com.ezylang.evalex.EvaluationException; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.parser.Token; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; + +import static java.math.BigDecimal.valueOf; + +/** + * Abstract implementation of the {@link FunctionIfc}, used as base class for function + * implementations. + */ +public abstract class AbstractFunction implements FunctionIfc { + + protected static final BigDecimal MINUS_ONE = valueOf(-1); + private final List functionParameterDefinitions = new ArrayList<>(); + + private final boolean hasVarArgs; + + /** + * Creates a new function and uses the {@link FunctionParameter} annotations to create the + * parameter definitions. + */ + protected AbstractFunction() { + FunctionParameter[] parameterAnnotations = + getClass().getAnnotationsByType(FunctionParameter.class); + + boolean varArgParameterFound = false; + + for (FunctionParameter parameter : parameterAnnotations) { + if (varArgParameterFound) { + throw new IllegalArgumentException( + "Only last parameter may be defined as variable argument"); + } + if (parameter.isVarArg()) { + varArgParameterFound = true; + } + functionParameterDefinitions.add( + FunctionParameterDefinition.builder() + .name(parameter.name()) + .isVarArg(parameter.isVarArg()) + .isLazy(parameter.isLazy()) + .nonZero(parameter.nonZero()) + .nonNegative(parameter.nonNegative()) + .build()); + } + + hasVarArgs = varArgParameterFound; + } + + @Override + public void validatePreEvaluation(Token token, EvaluationValue... parameterValues) + throws EvaluationException { + + for (int i = 0; i < parameterValues.length; i++) { + FunctionParameterDefinition definition = getParameterDefinitionForParameter(i); + if (definition.isNonZero() && parameterValues[i].getNumberValue().equals(BigDecimal.ZERO)) { + throw new EvaluationException(token, "Parameter must not be zero"); + } + if (definition.isNonNegative() && parameterValues[i].getNumberValue().signum() < 0) { + throw new EvaluationException(token, "Parameter must not be negative"); + } + } + } + + @Override + public List getFunctionParameterDefinitions() { + return functionParameterDefinitions; + } + + @Override + public boolean hasVarArgs() { + return hasVarArgs; + } + + private FunctionParameterDefinition getParameterDefinitionForParameter(int index) { + + if (hasVarArgs && index >= functionParameterDefinitions.size()) { + index = functionParameterDefinitions.size() - 1; + } + + return functionParameterDefinitions.get(index); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/FunctionIfc.java b/src/main/java/com/ezylang/evalex/functions/FunctionIfc.java new file mode 100644 index 000000000..5d0070052 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/FunctionIfc.java @@ -0,0 +1,94 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.functions; + +import com.ezylang.evalex.EvaluationException; +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.parser.Token; + +import java.util.List; + +/** + * Interface that is required for all functions in a function dictionary for evaluation of + * expressions. + */ +public interface FunctionIfc { + + /** + * Returns the list of parameter definitions. Is never empty or null. + * + * @return The parameter definition list. + */ + List getFunctionParameterDefinitions(); + + /** + * Performs the function logic and returns an evaluation result. + * + * @param expression The expression, where this function is executed. Can be used to access the + * expression configuration. + * @param functionToken The function token from the parsed expression. + * @param parameterValues The parameter values. + * @return The evaluation result in form of a {@link EvaluationValue}. + * @throws EvaluationException In case there were problems during evaluation. + */ + EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) + throws EvaluationException; + + /** + * Validates the evaluation parameters, called before the actual evaluation. + * + * @param token The function token. + * @param parameterValues The parameter values + * @throws EvaluationException in case of any validation error + */ + void validatePreEvaluation(Token token, EvaluationValue... parameterValues) + throws EvaluationException; + + /** + * Checks whether the function has a variable number of arguments parameter. + * + * @return true or false: + */ + boolean hasVarArgs(); + + /** + * Checks if the parameter is a lazy parameter. + * + * @param parameterIndex The parameter index, starts at 0 for the first parameter. If the index is + * bigger than the list of parameter definitions, the last parameter definition will be + * checked. + * @return true if the specified parameter is defined as lazy. + */ + default boolean isParameterLazy(int parameterIndex) { + if (parameterIndex >= getFunctionParameterDefinitions().size()) { + parameterIndex = getFunctionParameterDefinitions().size() - 1; + } + return getFunctionParameterDefinitions().get(parameterIndex).isLazy(); + } + + /** + * Returns the count of non-var-arg parameters defined by this function. If the function has + * var-args, the result is the count of parameter definitions - 1. + * + * @return the count of non-var-arg parameters defined by this function. + */ + default int getCountOfNonVarArgParameters() { + int numOfParameters = getFunctionParameterDefinitions().size(); + return hasVarArgs() ? numOfParameters - 1 : numOfParameters; + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/FunctionParameter.java b/src/main/java/com/ezylang/evalex/functions/FunctionParameter.java new file mode 100644 index 000000000..37b8b93f1 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/FunctionParameter.java @@ -0,0 +1,58 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.functions; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation to define a function parameter. + */ +@Documented +@Target(ElementType.TYPE) +@Repeatable(FunctionParameters.class) +@Retention(RetentionPolicy.RUNTIME) +public @interface FunctionParameter { + + /** + * The parameter name. + */ + String name(); + + /** + * If the parameter is lazily evaluated. Defaults to false. + */ + boolean isLazy() default false; + + /** + * If the parameter is a variable arg type (repeatable). Defaults to false. + */ + boolean isVarArg() default false; + + /** + * If the parameter does not allow zero values. + */ + boolean nonZero() default false; + + /** + * If the parameter does not allow negative values. + */ + boolean nonNegative() default false; +} diff --git a/src/main/java/com/ezylang/evalex/functions/FunctionParameterDefinition.java b/src/main/java/com/ezylang/evalex/functions/FunctionParameterDefinition.java new file mode 100644 index 000000000..329443b3f --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/FunctionParameterDefinition.java @@ -0,0 +1,57 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.functions; + +import lombok.Builder; +import lombok.Value; + +/** + * Definition of a function parameter. + */ +@Value +@Builder +public class FunctionParameterDefinition { + + /** + * Name of the parameter, useful for error messages etc. + */ + String name; + + /** + * Whether this parameter is a variable argument parameter (can be repeated). + * + * @see com.ezylang.evalex.functions.basic.MinFunction for an example. + */ + boolean isVarArg; + + /** + * Set to true, the parameter will not be evaluated in advance, but the corresponding {@link + * com.ezylang.evalex.parser.ASTNode} will be passed as a parameter value. + * + * @see com.ezylang.evalex.functions.basic.IfFunction for an example. + */ + boolean isLazy; + + /** + * If the parameter does not allow zero values. + */ + boolean nonZero; + + /** + * If the parameter does not allow negative values. + */ + boolean nonNegative; +} diff --git a/src/main/java/com/ezylang/evalex/functions/FunctionParameters.java b/src/main/java/com/ezylang/evalex/functions/FunctionParameters.java new file mode 100644 index 000000000..1376a2045 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/FunctionParameters.java @@ -0,0 +1,33 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.functions; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Collator for repeatable {@link FunctionParameter} annotations. + */ +@Documented +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface FunctionParameters { + + FunctionParameter[] value(); +} diff --git a/src/main/java/com/ezylang/evalex/functions/basic/AbsFunction.java b/src/main/java/com/ezylang/evalex/functions/basic/AbsFunction.java new file mode 100644 index 000000000..df0b2483d --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/basic/AbsFunction.java @@ -0,0 +1,37 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.functions.basic; + +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.functions.AbstractFunction; +import com.ezylang.evalex.functions.FunctionParameter; +import com.ezylang.evalex.parser.Token; + +/** + * Absolute (non-negative) value. + */ +@FunctionParameter(name = "value") +public class AbsFunction extends AbstractFunction { + + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + + return expression.convertValue( + parameterValues[0].getNumberValue().abs(expression.getConfiguration().getMathContext())); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/basic/AbstractMinMaxFunction.java b/src/main/java/com/ezylang/evalex/functions/basic/AbstractMinMaxFunction.java new file mode 100644 index 000000000..afb8e0370 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/basic/AbstractMinMaxFunction.java @@ -0,0 +1,45 @@ +/* + Copyright 2012-2024 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.functions.basic; + +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.functions.AbstractFunction; +import com.ezylang.evalex.functions.FunctionParameter; + +import java.math.BigDecimal; + +@FunctionParameter(name = "value", isVarArg = true) +public abstract class AbstractMinMaxFunction extends AbstractFunction { + + BigDecimal findMinOrMax(BigDecimal current, EvaluationValue parameter, boolean findMin) { + if (parameter.isArrayValue()) { + for (EvaluationValue element : parameter.getArrayValue()) { + current = findMinOrMax(current, element, findMin); + } + } else { + current = compareAndAssign(current, parameter.getNumberValue(), findMin); + } + return current; + } + + BigDecimal compareAndAssign(BigDecimal current, BigDecimal newValue, boolean findMin) { + if (current == null + || (findMin ? newValue.compareTo(current) < 0 : newValue.compareTo(current) > 0)) { + current = newValue; + } + return current; + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/basic/AverageFunction.java b/src/main/java/com/ezylang/evalex/functions/basic/AverageFunction.java new file mode 100644 index 000000000..63f1092a6 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/basic/AverageFunction.java @@ -0,0 +1,82 @@ +/* + Copyright 2012-2024 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.functions.basic; + +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.functions.FunctionParameter; +import com.ezylang.evalex.parser.Token; + +import java.math.BigDecimal; +import java.math.MathContext; + +/** + * Returns the average (arithmetic mean) of the numeric arguments, with recursive support for + * arrays. + * + * @author oswaldo.bapvic.jr + */ +@FunctionParameter(name = "firstValue") +@FunctionParameter(name = "additionalValues", isVarArg = true) +public class AverageFunction extends AbstractMinMaxFunction { + + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + MathContext mathContext = expression.getConfiguration().getMathContext(); + BigDecimal average = average(mathContext, parameterValues); + return expression.convertValue(average); + } + + private BigDecimal average(MathContext mathContext, EvaluationValue... parameterValues) { + SumAndCount aux = new SumAndCount(); + for (EvaluationValue parameter : parameterValues) { + aux = aux.plus(recursiveSumAndCount(parameter)); + } + + return aux.sum.divide(aux.count, mathContext); + } + + private SumAndCount recursiveSumAndCount(EvaluationValue parameter) { + SumAndCount aux = new SumAndCount(); + if (parameter.isArrayValue()) { + for (EvaluationValue element : parameter.getArrayValue()) { + aux = aux.plus(recursiveSumAndCount(element)); + } + return aux; + } + return new SumAndCount(parameter.getNumberValue(), BigDecimal.ONE); + } + + private final class SumAndCount { + + private final BigDecimal sum; + private final BigDecimal count; + + private SumAndCount() { + this(BigDecimal.ZERO, BigDecimal.ZERO); + } + + private SumAndCount(BigDecimal sum, BigDecimal count) { + this.sum = sum; + this.count = count; + } + + private SumAndCount plus(SumAndCount other) { + return new SumAndCount(sum.add(other.sum), count.add(other.count)); + } + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/basic/CeilingFunction.java b/src/main/java/com/ezylang/evalex/functions/basic/CeilingFunction.java new file mode 100644 index 000000000..189f655d5 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/basic/CeilingFunction.java @@ -0,0 +1,40 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.functions.basic; + +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.functions.AbstractFunction; +import com.ezylang.evalex.functions.FunctionParameter; +import com.ezylang.evalex.parser.Token; + +import java.math.RoundingMode; + +/** + * Rounds the given value to an integer using the rounding mode {@link RoundingMode#CEILING} + */ +@FunctionParameter(name = "value") +public class CeilingFunction extends AbstractFunction { + + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + + EvaluationValue value = parameterValues[0]; + + return expression.convertValue(value.getNumberValue().setScale(0, RoundingMode.CEILING)); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/basic/CoalesceFunction.java b/src/main/java/com/ezylang/evalex/functions/basic/CoalesceFunction.java new file mode 100644 index 000000000..584e6fdde --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/basic/CoalesceFunction.java @@ -0,0 +1,41 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.functions.basic; + +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.functions.AbstractFunction; +import com.ezylang.evalex.functions.FunctionParameter; +import com.ezylang.evalex.parser.Token; + +/** + * Returns the first non-null parameter, or {@link EvaluationValue#NULL_VALUE} if all parameters are + * null. + */ +@FunctionParameter(name = "value", isVarArg = true) +public class CoalesceFunction extends AbstractFunction { + + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + for (EvaluationValue parameter : parameterValues) { + if (!parameter.isNullValue()) { + return parameter; + } + } + return EvaluationValue.NULL_VALUE; + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/basic/FactFunction.java b/src/main/java/com/ezylang/evalex/functions/basic/FactFunction.java new file mode 100644 index 000000000..b66198a18 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/basic/FactFunction.java @@ -0,0 +1,45 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.functions.basic; + +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.functions.AbstractFunction; +import com.ezylang.evalex.functions.FunctionParameter; +import com.ezylang.evalex.parser.Token; + +import java.math.BigDecimal; + +/** + * Factorial function, calculates the factorial of a base value. + */ +@FunctionParameter(name = "base") +public class FactFunction extends AbstractFunction { + + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + int number = parameterValues[0].getNumberValue().intValue(); + BigDecimal factorial = BigDecimal.ONE; + for (int i = 1; i <= number; i++) { + factorial = + factorial.multiply( + new BigDecimal(i, expression.getConfiguration().getMathContext()), + expression.getConfiguration().getMathContext()); + } + return expression.convertValue(factorial); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/basic/FloorFunction.java b/src/main/java/com/ezylang/evalex/functions/basic/FloorFunction.java new file mode 100644 index 000000000..d7d6e4758 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/basic/FloorFunction.java @@ -0,0 +1,40 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.functions.basic; + +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.functions.AbstractFunction; +import com.ezylang.evalex.functions.FunctionParameter; +import com.ezylang.evalex.parser.Token; + +import java.math.RoundingMode; + +/** + * Rounds the given value to an integer using the rounding mode {@link RoundingMode#FLOOR} + */ +@FunctionParameter(name = "value") +public class FloorFunction extends AbstractFunction { + + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + + EvaluationValue value = parameterValues[0]; + + return expression.convertValue(value.getNumberValue().setScale(0, RoundingMode.FLOOR)); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/basic/IfFunction.java b/src/main/java/com/ezylang/evalex/functions/basic/IfFunction.java new file mode 100644 index 000000000..b6106b3fe --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/basic/IfFunction.java @@ -0,0 +1,46 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.functions.basic; + +import com.ezylang.evalex.EvaluationException; +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.functions.AbstractFunction; +import com.ezylang.evalex.functions.FunctionParameter; +import com.ezylang.evalex.parser.Token; + +/** + * Conditional evaluation function. If parameter condition is true, the + * resultIfTrue value is returned, else the resultIfFalse value. + * resultIfTrue and resultIfFalse are only evaluated (lazily evaluated), + * after the condition was evaluated. + */ +@FunctionParameter(name = "condition") +@FunctionParameter(name = "resultIfTrue", isLazy = true) +@FunctionParameter(name = "resultIfFalse", isLazy = true) +public class IfFunction extends AbstractFunction { + + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) + throws EvaluationException { + if (Boolean.TRUE.equals(parameterValues[0].getBooleanValue())) { + return expression.evaluateSubtree(parameterValues[1].getExpressionNode()); + } else { + return expression.evaluateSubtree(parameterValues[2].getExpressionNode()); + } + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/basic/Log10Function.java b/src/main/java/com/ezylang/evalex/functions/basic/Log10Function.java new file mode 100644 index 000000000..35c078a7a --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/basic/Log10Function.java @@ -0,0 +1,38 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.functions.basic; + +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.functions.AbstractFunction; +import com.ezylang.evalex.functions.FunctionParameter; +import com.ezylang.evalex.parser.Token; + +/** + * The base 10 logarithm of a value + */ +@FunctionParameter(name = "value", nonZero = true, nonNegative = true) +public class Log10Function extends AbstractFunction { + + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + + double d = parameterValues[0].getNumberValue().doubleValue(); + + return expression.convertDoubleValue(Math.log10(d)); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/basic/LogFunction.java b/src/main/java/com/ezylang/evalex/functions/basic/LogFunction.java new file mode 100644 index 000000000..e0110aec3 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/basic/LogFunction.java @@ -0,0 +1,38 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.functions.basic; + +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.functions.AbstractFunction; +import com.ezylang.evalex.functions.FunctionParameter; +import com.ezylang.evalex.parser.Token; + +/** + * The natural logarithm (base e) of a value + */ +@FunctionParameter(name = "value", nonZero = true, nonNegative = true) +public class LogFunction extends AbstractFunction { + + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + + double d = parameterValues[0].getNumberValue().doubleValue(); + + return expression.convertDoubleValue(Math.log(d)); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/basic/MaxFunction.java b/src/main/java/com/ezylang/evalex/functions/basic/MaxFunction.java new file mode 100644 index 000000000..2beefa4b8 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/basic/MaxFunction.java @@ -0,0 +1,41 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.functions.basic; + +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.functions.FunctionParameter; +import com.ezylang.evalex.parser.Token; + +import java.math.BigDecimal; + +/** + * Returns the maximum value of all parameters. + */ +@FunctionParameter(name = "firstValue") +@FunctionParameter(name = "value", isVarArg = true) +public class MaxFunction extends AbstractMinMaxFunction { + + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + BigDecimal min = null; + for (EvaluationValue parameter : parameterValues) { + min = findMinOrMax(min, parameter, false); + } + return expression.convertValue(min); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/basic/MinFunction.java b/src/main/java/com/ezylang/evalex/functions/basic/MinFunction.java new file mode 100644 index 000000000..44c7c8d17 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/basic/MinFunction.java @@ -0,0 +1,41 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.functions.basic; + +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.functions.FunctionParameter; +import com.ezylang.evalex.parser.Token; + +import java.math.BigDecimal; + +/** + * Returns the minimum value of all parameters. + */ +@FunctionParameter(name = "firstValue") +@FunctionParameter(name = "value", isVarArg = true) +public class MinFunction extends AbstractMinMaxFunction { + + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + BigDecimal min = null; + for (EvaluationValue parameter : parameterValues) { + min = findMinOrMax(min, parameter, true); + } + return expression.convertValue(min); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/basic/NotFunction.java b/src/main/java/com/ezylang/evalex/functions/basic/NotFunction.java new file mode 100644 index 000000000..f7c590390 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/basic/NotFunction.java @@ -0,0 +1,38 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.functions.basic; + +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.functions.AbstractFunction; +import com.ezylang.evalex.functions.FunctionParameter; +import com.ezylang.evalex.parser.Token; + +/** + * Boolean negation function. + */ +@FunctionParameter(name = "value") +public class NotFunction extends AbstractFunction { + + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + + boolean result = parameterValues[0].getBooleanValue(); + + return expression.convertValue(!result); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/basic/RandomFunction.java b/src/main/java/com/ezylang/evalex/functions/basic/RandomFunction.java new file mode 100644 index 000000000..8e047e759 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/basic/RandomFunction.java @@ -0,0 +1,38 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.functions.basic; + +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.functions.AbstractFunction; +import com.ezylang.evalex.parser.Token; + +import java.security.SecureRandom; + +/** + * Random function produces a random value between 0 and 1. + */ +public class RandomFunction extends AbstractFunction { + + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + + SecureRandom secureRandom = new SecureRandom(); + + return expression.convertDoubleValue(secureRandom.nextDouble()); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/basic/RoundFunction.java b/src/main/java/com/ezylang/evalex/functions/basic/RoundFunction.java new file mode 100644 index 000000000..449580f03 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/basic/RoundFunction.java @@ -0,0 +1,46 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.functions.basic; + +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.functions.AbstractFunction; +import com.ezylang.evalex.functions.FunctionParameter; +import com.ezylang.evalex.parser.Token; + +/** + * Rounds the given value to the specified scale, using the {@link java.math.MathContext} of the + * expression configuration. + */ +@FunctionParameter(name = "value") +@FunctionParameter(name = "scale") +public class RoundFunction extends AbstractFunction { + + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + + EvaluationValue value = parameterValues[0]; + EvaluationValue precision = parameterValues[1]; + + return expression.convertValue( + value + .getNumberValue() + .setScale( + precision.getNumberValue().intValue(), + expression.getConfiguration().getMathContext().getRoundingMode())); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/basic/SqrtFunction.java b/src/main/java/com/ezylang/evalex/functions/basic/SqrtFunction.java new file mode 100644 index 000000000..2ad0bd175 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/basic/SqrtFunction.java @@ -0,0 +1,66 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.functions.basic; + +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.functions.AbstractFunction; +import com.ezylang.evalex.functions.FunctionParameter; +import com.ezylang.evalex.parser.Token; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.math.MathContext; + +/** + * Square root function, uses the implementation from The Java Programmers Guide To numerical + * Computing by Ronald Mak, 2002. + */ +@FunctionParameter(name = "value", nonNegative = true) +public class SqrtFunction extends AbstractFunction { + + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + + /* + * From The Java Programmers Guide To numerical Computing + * (Ronald Mak, 2002) + */ + + BigDecimal x = parameterValues[0].getNumberValue(); + MathContext mathContext = expression.getConfiguration().getMathContext(); + + if (x.compareTo(BigDecimal.ZERO) == 0) { + return expression.convertValue(BigDecimal.ZERO); + } + BigInteger n = x.movePointRight(mathContext.getPrecision() << 1).toBigInteger(); + + int bits = (n.bitLength() + 1) >> 1; + BigInteger ix = n.shiftRight(bits); + BigInteger ixPrev; + BigInteger test; + do { + ixPrev = ix; + ix = ix.add(n.divide(ix)).shiftRight(1); + // Give other threads a chance to work + Thread.yield(); + test = ix.subtract(ixPrev).abs(); + } while (test.compareTo(BigInteger.ZERO) != 0 && test.compareTo(BigInteger.ONE) != 0); + + return expression.convertValue(new BigDecimal(ix, mathContext.getPrecision())); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/basic/SumFunction.java b/src/main/java/com/ezylang/evalex/functions/basic/SumFunction.java new file mode 100644 index 000000000..74ed2db2e --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/basic/SumFunction.java @@ -0,0 +1,57 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.functions.basic; + +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.functions.AbstractFunction; +import com.ezylang.evalex.functions.FunctionParameter; +import com.ezylang.evalex.parser.Token; + +import java.math.BigDecimal; + +/** + * Returns the sum value of all parameters. + */ +@FunctionParameter(name = "value", isVarArg = true) +public class SumFunction extends AbstractFunction { + + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + BigDecimal sum = BigDecimal.ZERO; + for (EvaluationValue parameter : parameterValues) { + sum = + sum.add( + recursiveSum(parameter, expression), expression.getConfiguration().getMathContext()); + } + return expression.convertValue(sum); + } + + private BigDecimal recursiveSum(EvaluationValue parameter, Expression expression) { + BigDecimal sum = BigDecimal.ZERO; + if (parameter.isArrayValue()) { + for (EvaluationValue element : parameter.getArrayValue()) { + sum = + sum.add( + recursiveSum(element, expression), expression.getConfiguration().getMathContext()); + } + } else { + sum = sum.add(parameter.getNumberValue(), expression.getConfiguration().getMathContext()); + } + return sum; + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/basic/SwitchFunction.java b/src/main/java/com/ezylang/evalex/functions/basic/SwitchFunction.java new file mode 100644 index 000000000..5755e374c --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/basic/SwitchFunction.java @@ -0,0 +1,102 @@ +/* + Copyright 2012-2024 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.functions.basic; + +import com.ezylang.evalex.EvaluationException; +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.functions.AbstractFunction; +import com.ezylang.evalex.functions.FunctionParameter; +import com.ezylang.evalex.parser.Token; + +/** + * A function that evaluates one value (or expression) against a list of values, and returns the + * result corresponding to the first matching value. If there is no match, an optional default value + * may be returned. + * + *

Syntax: + * + *

+ *

+ * {@code SWITCH(expression, value1, result1, [value, result, ...], [default])} + * + *

+ * + *

Examples: + * + *

1. The following function will return either "Sunday", "Monday", or "Tuesday", depending on + * the result of the variable {@code weekday}. Since no default value was specified, the function + * will return a null value if there is no match: + * + *

+ *

+ * {@code SWITCH(weekday, 1, "Sunday", 2, "Monday", 3, "Tuesday")} + * + *

+ * + *

2. The following function will return either "Sunday", "Monday", "Tuesday", or "No match", + * depending on the result of the variable {@code weekday}: + * + *

+ *

+ * {@code SWITCH(weekday, 1, "Sunday", 2, "Monday", 3, "Tuesday", "No match")} + * + *

+ * + * @author oswaldo.bapvic.jr + */ +@FunctionParameter(name = "expression") +@FunctionParameter(name = "value1") +@FunctionParameter(name = "result1", isLazy = true) +@FunctionParameter(name = "additionalValues", isLazy = true, isVarArg = true) +public class SwitchFunction extends AbstractFunction { + + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) + throws EvaluationException { + + EvaluationValue result = EvaluationValue.NULL_VALUE; + + // First get the first parameter + EvaluationValue value = parameterValues[0]; + + // Iterate through the parameters to parse the pairs of value-result and the default result if + // present. + int index = 1; + while (index < parameterValues.length) { + int next = index + 1; + if (next < parameterValues.length) { + if (value.equals(evaluateParameter(expression, parameterValues[index]))) { + result = parameterValues[next]; + break; + } + index += 2; + } else { + // The default result + result = parameterValues[index++]; + } + } + return evaluateParameter(expression, result); + } + + private EvaluationValue evaluateParameter(Expression expression, EvaluationValue parameter) + throws EvaluationException { + return parameter.isExpressionNode() + ? expression.evaluateSubtree(parameter.getExpressionNode()) + : parameter; + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/datetime/DateTimeFormatFunction.java b/src/main/java/com/ezylang/evalex/functions/datetime/DateTimeFormatFunction.java new file mode 100644 index 000000000..2b2302936 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/datetime/DateTimeFormatFunction.java @@ -0,0 +1,74 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.functions.datetime; + +import com.ezylang.evalex.EvaluationException; +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.functions.AbstractFunction; +import com.ezylang.evalex.functions.FunctionParameter; +import com.ezylang.evalex.parser.Token; + +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; + +/** + * Function to format a DATE_TIME vale. Required parameter is the DATE_TIME value to format. First + * optional parameter is the format to use, using a pattern used by {@link DateTimeFormatter}. If no + * format is given, the first format defined in the configured formats is used. Second optional + * parameter is the zone-id to use with formatting. Default is the configured zone-id. + */ +@FunctionParameter(name = "value") +@FunctionParameter(name = "parameters", isVarArg = true) +public class DateTimeFormatFunction extends AbstractFunction { + + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) + throws EvaluationException { + + DateTimeFormatter formatter = expression.getConfiguration().getDateTimeFormatters().get(0); + if (parameterValues.length > 1) { + formatter = + DateTimeFormatter.ofPattern(parameterValues[1].getStringValue()) + .withLocale(expression.getConfiguration().getLocale()); + } + + ZoneId zoneId = expression.getConfiguration().getZoneId(); + if (parameterValues.length == 3) { + zoneId = ZoneIdConverter.convert(functionToken, parameterValues[2].getStringValue()); + } + + return expression.convertValue( + parameterValues[0].getDateTimeValue().atZone(zoneId).format(formatter)); + } + + @Override + public void validatePreEvaluation(Token token, EvaluationValue... parameterValues) + throws EvaluationException { + super.validatePreEvaluation(token, parameterValues); + if (parameterValues.length > 3) { + throw new EvaluationException(token, "Too many parameters"); + } + if (!parameterValues[0].isDateTimeValue()) { + throw new EvaluationException( + token, + String.format( + "Unable to format a '%s' type as a date-time", + parameterValues[0].getDataType().name())); + } + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/datetime/DateTimeNewFunction.java b/src/main/java/com/ezylang/evalex/functions/datetime/DateTimeNewFunction.java new file mode 100644 index 000000000..5812256cd --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/datetime/DateTimeNewFunction.java @@ -0,0 +1,124 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.functions.datetime; + +import com.ezylang.evalex.EvaluationException; +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.functions.AbstractFunction; +import com.ezylang.evalex.functions.FunctionParameter; +import com.ezylang.evalex.parser.Token; +import org.apache.commons.lang3.ArrayUtils; + +import java.math.BigDecimal; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.util.TimeZone; + +/** + * Creates a new DATE_TIME value with the given parameters. If only one parameter is given, it is + * treated as the time in milliseconds from the epoch of 1970-01-01T00:00:00Z and a corresponding + * date/time value is created. Else, A minimum of three parameters (year, month, day) must be + * specified. Optionally the hour, minute, second and nanosecond can be specified. If the last + * parameter is a string value, it is treated as a zone ID. If no zone ID is specified, the + * configured zone ID is used. + */ +@FunctionParameter(name = "values", isVarArg = true, nonNegative = true) +public class DateTimeNewFunction extends AbstractFunction { + + private static String[] timeZones = null; + + public static String[] getTimeZones() { + if (timeZones == null) { + timeZones = TimeZone.getAvailableIDs(); + } + return timeZones; + } + + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) + throws EvaluationException { + + int parameterLength = parameterValues.length; + + if (parameterLength == 1) { + BigDecimal millis = parameterValues[0].getNumberValue(); + return expression.convertValue(Instant.ofEpochMilli(millis.longValue())); + } + + ZoneId zoneId = expression.getConfiguration().getZoneId(); + if (parameterValues[parameterLength - 1].isStringValue()) { + zoneId = + ZoneIdConverter.convert( + functionToken, parameterValues[parameterLength - 1].getStringValue()); + parameterLength--; + } + + int year = parameterValues[0].getNumberValue().intValue(); + int month = parameterValues[1].getNumberValue().intValue(); + int day = parameterValues[2].getNumberValue().intValue(); + int hour = parameterLength >= 4 ? parameterValues[3].getNumberValue().intValue() : 0; + int minute = parameterLength >= 5 ? parameterValues[4].getNumberValue().intValue() : 0; + int second = parameterLength >= 6 ? parameterValues[5].getNumberValue().intValue() : 0; + int nanoOfs = parameterLength == 7 ? parameterValues[6].getNumberValue().intValue() : 0; + + return expression.convertValue( + LocalDateTime.of(year, month, day, hour, minute, second, nanoOfs) + .atZone(zoneId) + .toInstant()); + } + + @Override + public void validatePreEvaluation(Token token, EvaluationValue... parameterValues) + throws EvaluationException { + + super.validatePreEvaluation(token, parameterValues); + + int parameterLength = parameterValues.length; + + if (parameterLength == 0) { + throw new EvaluationException(token, "Not enough parameters for function"); + } + + if (parameterLength == 1) { + if (!parameterValues[0].isNumberValue()) { + throw new EvaluationException( + token, "Expected a number value for the time in milliseconds since the epoch"); + } else { + return; + } + } + + if (parameterValues[parameterLength - 1].isStringValue()) { + if (!ArrayUtils.contains(getTimeZones(), parameterValues[parameterLength - 1].getStringValue())) { + throw new EvaluationException(token, "Time zone with id '" + + parameterValues[parameterLength - 1].getStringValue() + "' not found"); + } + parameterLength--; + } + + if (parameterLength < 3) { + throw new EvaluationException( + token, "A minimum of 3 parameters (year, month, day) is required"); + } + + if (parameterLength > 7) { + throw new EvaluationException(token, "Too many parameters to function"); + } + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/datetime/DateTimeNowFunction.java b/src/main/java/com/ezylang/evalex/functions/datetime/DateTimeNowFunction.java new file mode 100644 index 000000000..e3a2960ca --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/datetime/DateTimeNowFunction.java @@ -0,0 +1,47 @@ +/* + Copyright 2012-2024 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.functions.datetime; + +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.functions.AbstractFunction; +import com.ezylang.evalex.parser.Token; + +import java.time.Instant; + +/** + * Produces a new DATE_TIME that represents the current date and time. + * + *

It is useful to calculate a value based on the current date and time. For example, if you know + * the start DATE_TIME of a running process, you might use the following expression to find the + * DURATION that represents the process age: + * + *

+ *

+ * {@code DT_NOW() - startDateTime} + * + *

+ * + * @author oswaldobapvicjr + */ +public class DateTimeNowFunction extends AbstractFunction { + + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + return expression.convertValue(Instant.now()); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/datetime/DateTimeParseFunction.java b/src/main/java/com/ezylang/evalex/functions/datetime/DateTimeParseFunction.java new file mode 100644 index 000000000..dda9d60d9 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/datetime/DateTimeParseFunction.java @@ -0,0 +1,85 @@ +/* + Copyright 2012-2023 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.functions.datetime; + +import com.ezylang.evalex.EvaluationException; +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.config.ExpressionConfiguration; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.data.conversion.DateTimeConverter; +import com.ezylang.evalex.functions.AbstractFunction; +import com.ezylang.evalex.functions.FunctionParameter; +import com.ezylang.evalex.parser.Token; + +import java.time.Instant; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.List; + +/** + * Parses a date-time string to a {@link EvaluationValue.DataType#DATE_TIME} value. + * + *

Optional arguments are the time zone and a list of {@link java.time.format.DateTimeFormatter} + * patterns. Each pattern will be tried to convert the string to a date-time. The first matching + * pattern will be used. If NULL is specified for the time zone, the currently + * configured zone is used. If no formatter is specified, the function will use the formatters + * defined at the {@link ExpressionConfiguration}. + */ +@FunctionParameter(name = "value") +@FunctionParameter(name = "parameters", isVarArg = true) +public class DateTimeParseFunction extends AbstractFunction { + + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) + throws EvaluationException { + + String value = parameterValues[0].getStringValue(); + + ZoneId zoneId = expression.getConfiguration().getZoneId(); + if (parameterValues.length > 1 && !parameterValues[1].isNullValue()) { + zoneId = ZoneIdConverter.convert(functionToken, parameterValues[1].getStringValue()); + } + + List formatters; + + if (parameterValues.length > 2) { + formatters = new ArrayList<>(); + for (int i = 2; i < parameterValues.length; i++) { + try { + formatters.add(DateTimeFormatter.ofPattern(parameterValues[i].getStringValue())); + } catch (IllegalArgumentException ex) { + throw new EvaluationException( + functionToken, + String.format( + "Illegal date-time format in parameter %d: '%s'", + i + 1, parameterValues[i].getStringValue())); + } + } + } else { + formatters = expression.getConfiguration().getDateTimeFormatters(); + } + DateTimeConverter converter = new DateTimeConverter(); + Instant instant = converter.parseDateTime(value, zoneId, formatters); + + if (instant == null) { + throw new EvaluationException( + functionToken, String.format("Unable to parse date-time string '%s'", value)); + } + return EvaluationValue.dateTimeValue(instant); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/datetime/DateTimeToEpochFunction.java b/src/main/java/com/ezylang/evalex/functions/datetime/DateTimeToEpochFunction.java new file mode 100644 index 000000000..3e0be8af8 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/datetime/DateTimeToEpochFunction.java @@ -0,0 +1,35 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.functions.datetime; + +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.functions.AbstractFunction; +import com.ezylang.evalex.functions.FunctionParameter; +import com.ezylang.evalex.parser.Token; + +/** + * Function to convert a DATE_TIME value to milliseconds in the epoch of 1970-01-01T00:00:00Z. + */ +@FunctionParameter(name = "value") +public class DateTimeToEpochFunction extends AbstractFunction { + + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + return expression.convertValue(parameterValues[0].getDateTimeValue().toEpochMilli()); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/datetime/DateTimeTodayFunction.java b/src/main/java/com/ezylang/evalex/functions/datetime/DateTimeTodayFunction.java new file mode 100644 index 000000000..f91e6c667 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/datetime/DateTimeTodayFunction.java @@ -0,0 +1,67 @@ +/* + Copyright 2012-2024 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.functions.datetime; + +import com.ezylang.evalex.EvaluationException; +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.config.ExpressionConfiguration; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.functions.AbstractFunction; +import com.ezylang.evalex.functions.FunctionParameter; +import com.ezylang.evalex.parser.Token; + +import java.time.Instant; +import java.time.LocalDate; +import java.time.ZoneId; + +/** + * Produces a new DATE_TIME that represents the current date, at midnight (00:00). + * + *

It is useful for DATE_TIME comparison, when the current time must not be considered. For + * example, in the expression: + * + *

+ *

+ * {@code IF(expiryDate > DT_TODAY(), "expired", "valid")} + * + *

+ * + *

This function may accept an optional time zone to be applied. If no zone ID is specified, the + * default zone ID defined at the {@link ExpressionConfiguration} will be used. + * + * @author oswaldobapvicjr + */ +@FunctionParameter(name = "parameters", isVarArg = true) +public class DateTimeTodayFunction extends AbstractFunction { + + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) + throws EvaluationException { + ZoneId zoneId = parseZoneId(expression, functionToken, parameterValues); + Instant today = LocalDate.now().atStartOfDay(zoneId).toInstant(); + return expression.convertValue(today); + } + + private ZoneId parseZoneId( + Expression expression, Token functionToken, EvaluationValue... parameterValues) + throws EvaluationException { + if (parameterValues.length > 0 && !parameterValues[0].isNullValue()) { + return ZoneIdConverter.convert(functionToken, parameterValues[0].getStringValue()); + } + return expression.getConfiguration().getZoneId(); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/datetime/DurationFromMillisFunction.java b/src/main/java/com/ezylang/evalex/functions/datetime/DurationFromMillisFunction.java new file mode 100644 index 000000000..ecffc04ec --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/datetime/DurationFromMillisFunction.java @@ -0,0 +1,39 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.functions.datetime; + +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.functions.AbstractFunction; +import com.ezylang.evalex.functions.FunctionParameter; +import com.ezylang.evalex.parser.Token; + +import java.math.BigDecimal; +import java.time.Duration; + +/** + * Converts the given milliseconds to a DURATION value. + */ +@FunctionParameter(name = "value") +public class DurationFromMillisFunction extends AbstractFunction { + + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + BigDecimal millis = parameterValues[0].getNumberValue(); + return expression.convertValue(Duration.ofMillis(millis.longValue())); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/datetime/DurationNewFunction.java b/src/main/java/com/ezylang/evalex/functions/datetime/DurationNewFunction.java new file mode 100644 index 000000000..471662f86 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/datetime/DurationNewFunction.java @@ -0,0 +1,58 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.functions.datetime; + +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.functions.AbstractFunction; +import com.ezylang.evalex.functions.FunctionParameter; +import com.ezylang.evalex.parser.Token; + +import java.time.Duration; + +/** + * Function to create a new Duration. First parameter is required and specifies the number of days. + * All other parameters are optional and specify hours, minutes, seconds, milliseconds and + * nanoseconds. + */ +@FunctionParameter(name = "days") +@FunctionParameter(name = "parameters", isVarArg = true) +public class DurationNewFunction extends AbstractFunction { + + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + + int parameterLength = parameterValues.length; + + int days = parameterValues[0].getNumberValue().intValue(); + int hours = parameterLength >= 2 ? parameterValues[1].getNumberValue().intValue() : 0; + int minutes = parameterLength >= 3 ? parameterValues[2].getNumberValue().intValue() : 0; + int seconds = parameterLength >= 4 ? parameterValues[3].getNumberValue().intValue() : 0; + int millis = parameterLength >= 5 ? parameterValues[4].getNumberValue().intValue() : 0; + int nanos = parameterLength == 6 ? parameterValues[5].getNumberValue().intValue() : 0; + + Duration duration = + Duration.ofDays(days) + .plusHours(hours) + .plusMinutes(minutes) + .plusSeconds(seconds) + .plusMillis(millis) + .plusNanos(nanos); + + return expression.convertValue(duration); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/datetime/DurationParseFunction.java b/src/main/java/com/ezylang/evalex/functions/datetime/DurationParseFunction.java new file mode 100644 index 000000000..c4137092d --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/datetime/DurationParseFunction.java @@ -0,0 +1,39 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.functions.datetime; + +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.functions.AbstractFunction; +import com.ezylang.evalex.functions.FunctionParameter; +import com.ezylang.evalex.parser.Token; + +import java.time.Duration; + +/** + * Converts the given ISO-8601 duration string representation to a duration value. E.g. "P2DT3H4M" + * parses 2 days, 3 hours and 4 minutes. + */ +@FunctionParameter(name = "value") +public class DurationParseFunction extends AbstractFunction { + + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + String text = parameterValues[0].getStringValue(); + return expression.convertValue(Duration.parse(text)); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/datetime/DurationToMillisFunction.java b/src/main/java/com/ezylang/evalex/functions/datetime/DurationToMillisFunction.java new file mode 100644 index 000000000..d4aac1cb5 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/datetime/DurationToMillisFunction.java @@ -0,0 +1,35 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.functions.datetime; + +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.functions.AbstractFunction; +import com.ezylang.evalex.functions.FunctionParameter; +import com.ezylang.evalex.parser.Token; + +/** + * Converts a DURATION value to the amount of milliseconds. + */ +@FunctionParameter(name = "value") +public class DurationToMillisFunction extends AbstractFunction { + + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + return expression.convertValue(parameterValues[0].getDurationValue().toMillis()); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/datetime/ZoneIdConverter.java b/src/main/java/com/ezylang/evalex/functions/datetime/ZoneIdConverter.java new file mode 100644 index 000000000..842671451 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/datetime/ZoneIdConverter.java @@ -0,0 +1,53 @@ +/* + Copyright 2012-2023 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.functions.datetime; + +import com.ezylang.evalex.EvaluationException; +import com.ezylang.evalex.parser.Token; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +import java.time.DateTimeException; +import java.time.ZoneId; + +/** + * Validates and converts a zone ID. + */ +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class ZoneIdConverter { + + /** + * Converts a zone ID string to a {@link ZoneId}. Throws an {@link EvaluationException} if + * conversion fails. + * + * @param referenceToken The token for the error message, usually the function token. + * @param zoneIdString The zone IDS string to convert. + * @return The converted {@link ZoneId}. + * @throws EvaluationException In case the zone ID can't be converted. + */ + public static ZoneId convert(Token referenceToken, String zoneIdString) + throws EvaluationException { + try { + return ZoneId.of(zoneIdString); + } catch (DateTimeException exception) { + throw new EvaluationException( + referenceToken, + String.format( + "Unable to convert zone string '%s' to a zone ID: %s", + referenceToken.getValue(), exception.getMessage())); + } + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/string/StringContains.java b/src/main/java/com/ezylang/evalex/functions/string/StringContains.java new file mode 100644 index 000000000..d10842fe6 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/string/StringContains.java @@ -0,0 +1,42 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.functions.string; + +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.functions.AbstractFunction; +import com.ezylang.evalex.functions.FunctionParameter; +import com.ezylang.evalex.parser.Token; + +/** + * Returns true if the string contains the substring (case-insensitive). + */ +@FunctionParameter(name = "string") +@FunctionParameter(name = "substring") +public class StringContains extends AbstractFunction { + + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + String string = parameterValues[0].getStringValue(); + String substring = parameterValues[1].getStringValue(); + boolean result = + string != null + && substring != null + && string.toUpperCase().contains(substring.toUpperCase()); + return expression.convertValue(result); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/string/StringEndsWithFunction.java b/src/main/java/com/ezylang/evalex/functions/string/StringEndsWithFunction.java new file mode 100644 index 000000000..0b66002ac --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/string/StringEndsWithFunction.java @@ -0,0 +1,40 @@ +/* + Copyright 2012-2024 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.functions.string; + +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.functions.AbstractFunction; +import com.ezylang.evalex.functions.FunctionParameter; +import com.ezylang.evalex.parser.Token; + +/** + * Returns true if the string ends with the substring (case-sensitive). + * + * @author oswaldobapvicjr + */ +@FunctionParameter(name = "string") +@FunctionParameter(name = "substring") +public class StringEndsWithFunction extends AbstractFunction { + + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + String string = parameterValues[0].getStringValue(); + String substring = parameterValues[1].getStringValue(); + return expression.convertValue(string.endsWith(substring)); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/string/StringFormatFunction.java b/src/main/java/com/ezylang/evalex/functions/string/StringFormatFunction.java new file mode 100644 index 000000000..f92ffab6b --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/string/StringFormatFunction.java @@ -0,0 +1,87 @@ +/* + Copyright 2012-2024 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.functions.string; + +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.config.ExpressionConfiguration; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.functions.AbstractFunction; +import com.ezylang.evalex.functions.FunctionParameter; +import com.ezylang.evalex.parser.Token; + +import java.time.Instant; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.util.stream.IntStream; + +/** + * Returns a formatted string using the specified format string and arguments, using the configured + * locale. + * + *

For example: + * + *

+ *

+ * {@code STR_FORMAT("Welcome to %s!", "EvalEx")} + * + *

+ * + *

The result is produced using {@link String#format(String, Object...)}. + * + * @author oswaldobapvicjr + */ +@FunctionParameter(name = "format") +@FunctionParameter(name = "arguments", isVarArg = true) +public class StringFormatFunction extends AbstractFunction { + + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + String format = parameterValues[0].getStringValue(); + Object[] arguments = getFormatArguments(parameterValues, expression.getConfiguration()); + return expression.convertValue( + String.format(expression.getConfiguration().getLocale(), format, arguments)); + } + + private Object[] getFormatArguments( + EvaluationValue[] parameterValues, ExpressionConfiguration configuration) { + if (parameterValues.length > 1) { + return convertParametersToObjects(parameterValues, configuration); + } + return new Object[0]; + } + + private Object[] convertParametersToObjects( + EvaluationValue[] parameterValues, ExpressionConfiguration configuration) { + return IntStream.range(1, parameterValues.length) + .mapToObj(i -> convertParameterToObject(parameterValues[i], configuration)) + .toArray(); + } + + private Object convertParameterToObject( + EvaluationValue parameterValue, ExpressionConfiguration configuration) { + if (parameterValue.isDateTimeValue()) { + return convertInstantToLocalDateTime( + parameterValue.getDateTimeValue(), configuration.getZoneId()); + } else { + return parameterValue.getValue(); + } + } + + private ZonedDateTime convertInstantToLocalDateTime(Instant instant, ZoneId zoneId) { + return instant.atZone(zoneId); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/string/StringLeftFunction.java b/src/main/java/com/ezylang/evalex/functions/string/StringLeftFunction.java new file mode 100644 index 000000000..3e6db26ac --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/string/StringLeftFunction.java @@ -0,0 +1,64 @@ +/* + Copyright 2012-2024 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.functions.string; + +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.functions.AbstractFunction; +import com.ezylang.evalex.functions.FunctionParameter; +import com.ezylang.evalex.parser.Token; + +/** + * Represents a function that extracts a substring from the left side of a given string. This class + * extends the {@link AbstractFunction} and implements the logic for the `LEFT` string function, + * which returns a specified number of characters from the beginning (left) of the input string. + * + *

Two parameters are required for this function: + * + *

    + *
  • string - The input string from which the substring will be extracted. + *
  • length - The number of characters to extract from the left side of the string. If + * the specified length is greater than the string's length, the entire string is returned. If + * the length is negative or zero, an empty string is returned. + *
+ * + *

Example usage: If the input string is "hello" and the length is 2, the result will be "he". + */ +@FunctionParameter(name = "string") +@FunctionParameter(name = "length") +public class StringLeftFunction extends AbstractFunction { + + /** + * Evaluates the `LEFT` string function by extracting a substring from the left side of the given + * string. + * + * @param expression the current expression being evaluated + * @param functionToken the token representing the function being called + * @param parameterValues the parameters passed to the function; expects exactly two parameters: a + * string and a numeric value for length + * @return the substring extracted from the left side of the input string as an {@link + * EvaluationValue} + */ + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + String string = parameterValues[0].getStringValue(); + int length = + Math.max(0, Math.min(parameterValues[1].getNumberValue().intValue(), string.length())); + String substr = string.substring(0, length); + return expression.convertValue(substr); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/string/StringLengthFunction.java b/src/main/java/com/ezylang/evalex/functions/string/StringLengthFunction.java new file mode 100644 index 000000000..51abe6983 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/string/StringLengthFunction.java @@ -0,0 +1,39 @@ +/* + Copyright 2012-2024 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.functions.string; + +import com.ezylang.evalex.EvaluationException; +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.functions.AbstractFunction; +import com.ezylang.evalex.functions.FunctionParameter; +import com.ezylang.evalex.parser.Token; + +/** + * Returns the length of the string. + * + * @author HSGamer + */ +@FunctionParameter(name = "string") +public class StringLengthFunction extends AbstractFunction { + + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) + throws EvaluationException { + return expression.convertValue(parameterValues[0].getStringValue().length()); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/string/StringLowerFunction.java b/src/main/java/com/ezylang/evalex/functions/string/StringLowerFunction.java new file mode 100644 index 000000000..edb0a9699 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/string/StringLowerFunction.java @@ -0,0 +1,35 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.functions.string; + +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.functions.AbstractFunction; +import com.ezylang.evalex.functions.FunctionParameter; +import com.ezylang.evalex.parser.Token; + +/** + * Converts the given value to lower case. + */ +@FunctionParameter(name = "value") +public class StringLowerFunction extends AbstractFunction { + + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + return expression.convertValue(parameterValues[0].getStringValue().toLowerCase()); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/string/StringMatchesFunction.java b/src/main/java/com/ezylang/evalex/functions/string/StringMatchesFunction.java new file mode 100644 index 000000000..fceb02426 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/string/StringMatchesFunction.java @@ -0,0 +1,42 @@ +/* + Copyright 2012-2024 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.functions.string; + +import com.ezylang.evalex.EvaluationException; +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.functions.AbstractFunction; +import com.ezylang.evalex.functions.FunctionParameter; +import com.ezylang.evalex.parser.Token; + +/** + * Returns true if the string matches the pattern. + * + * @author HSGamer + */ +@FunctionParameter(name = "string") +@FunctionParameter(name = "pattern") +public class StringMatchesFunction extends AbstractFunction { + + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) + throws EvaluationException { + String string = parameterValues[0].getStringValue(); + String pattern = parameterValues[1].getStringValue(); + return expression.convertValue(string.matches(pattern)); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/string/StringRightFunction.java b/src/main/java/com/ezylang/evalex/functions/string/StringRightFunction.java new file mode 100644 index 000000000..8de28a3cf --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/string/StringRightFunction.java @@ -0,0 +1,64 @@ +/* + Copyright 2012-2024 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.functions.string; + +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.functions.AbstractFunction; +import com.ezylang.evalex.functions.FunctionParameter; +import com.ezylang.evalex.parser.Token; + +/** + * Represents a function that extracts a substring from the right side of a given string. This class + * extends the {@link AbstractFunction} and implements the logic for the `RIGHT` string function, + * which returns a specified number of characters from the end (right) of the input string. + * + *

Two parameters are required for this function: + * + *

    + *
  • string - The input string from which the substring will be extracted. + *
  • length - The number of characters to extract from the right side of the string. If + * the specified length is greater than the string's length, the entire string is returned. If + * the length is negative or zero, an empty string is returned. + *
+ * + *

Example usage: If the input string is "hello" and the length is 2, the result will be "lo". + */ +@FunctionParameter(name = "string") +@FunctionParameter(name = "length") +public class StringRightFunction extends AbstractFunction { + + /** + * Evaluates the `RIGHT` string function by extracting a substring from the right side of the + * given string. + * + * @param expression the current expression being evaluated + * @param functionToken the token representing the function being called + * @param parameterValues the parameters passed to the function; expects exactly two parameters: a + * string and a numeric value for length + * @return the substring extracted from the right side of the input string as an {@link + * EvaluationValue} + */ + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + String string = parameterValues[0].getStringValue(); + int length = + Math.max(0, Math.min(parameterValues[1].getNumberValue().intValue(), string.length())); + String substr = string.substring(string.length() - length); + return expression.convertValue(substr); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/string/StringSplitFunction.java b/src/main/java/com/ezylang/evalex/functions/string/StringSplitFunction.java new file mode 100644 index 000000000..d48449264 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/string/StringSplitFunction.java @@ -0,0 +1,53 @@ +/* + Copyright 2012-2025 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.functions.string; + +import com.ezylang.evalex.EvaluationException; +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.functions.AbstractFunction; +import com.ezylang.evalex.functions.FunctionParameter; +import com.ezylang.evalex.parser.Token; + +import java.util.regex.Pattern; + +/** + * A function that splits a string into an array, separators specified. + * + *

For example: + * + *

+ * + *

+ * STR_SPLIT("2024/07/15", "/")  = ["2024", "07", "15"]
+ * STR_SPLIT("myFile.json", ".") = ["myFile", "json"]
+ * 
+ * + * @author oswaldobapvicjr + */ +@FunctionParameter(name = "string") +@FunctionParameter(name = "separator") +public class StringSplitFunction extends AbstractFunction { + + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) + throws EvaluationException { + String string = parameterValues[0].getStringValue(); + String separator = parameterValues[1].getStringValue(); + return expression.convertValue(string.split(Pattern.quote(separator))); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/string/StringStartsWithFunction.java b/src/main/java/com/ezylang/evalex/functions/string/StringStartsWithFunction.java new file mode 100644 index 000000000..621f9aa71 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/string/StringStartsWithFunction.java @@ -0,0 +1,40 @@ +/* + Copyright 2012-2024 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.functions.string; + +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.functions.AbstractFunction; +import com.ezylang.evalex.functions.FunctionParameter; +import com.ezylang.evalex.parser.Token; + +/** + * Returns true if the string starts with the substring (case-sensitive). + * + * @author oswaldobapvicjr + */ +@FunctionParameter(name = "string") +@FunctionParameter(name = "substring") +public class StringStartsWithFunction extends AbstractFunction { + + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + String string = parameterValues[0].getStringValue(); + String substring = parameterValues[1].getStringValue(); + return expression.convertValue(string.startsWith(substring)); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/string/StringSubstringFunction.java b/src/main/java/com/ezylang/evalex/functions/string/StringSubstringFunction.java new file mode 100644 index 000000000..ab6120c6a --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/string/StringSubstringFunction.java @@ -0,0 +1,64 @@ +/* + Copyright 2012-2024 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.functions.string; + +import com.ezylang.evalex.EvaluationException; +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.functions.AbstractFunction; +import com.ezylang.evalex.functions.FunctionParameter; +import com.ezylang.evalex.parser.Token; + +/** + * Returns a substring of a string. + * + * @author HSGamer + */ +@FunctionParameter(name = "string") +@FunctionParameter(name = "start", nonNegative = true) +@FunctionParameter(name = "end", isVarArg = true, nonNegative = true) +public class StringSubstringFunction extends AbstractFunction { + + @Override + public void validatePreEvaluation(Token token, EvaluationValue... parameterValues) + throws EvaluationException { + super.validatePreEvaluation(token, parameterValues); + if (parameterValues.length > 2 + && parameterValues[2].getNumberValue().intValue() + < parameterValues[1].getNumberValue().intValue()) { + throw new EvaluationException( + token, "End index must be greater than or equal to start index"); + } + } + + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) + throws EvaluationException { + String string = parameterValues[0].getStringValue(); + int start = parameterValues[1].getNumberValue().intValue(); + String result; + if (parameterValues.length > 2) { + int end = parameterValues[2].getNumberValue().intValue(); + int length = string.length(); + int finalEnd = Math.min(end, length); + result = string.substring(start, finalEnd); + } else { + result = string.substring(start); + } + return expression.convertValue(result); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/string/StringTrimFunction.java b/src/main/java/com/ezylang/evalex/functions/string/StringTrimFunction.java new file mode 100644 index 000000000..d0a958e3a --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/string/StringTrimFunction.java @@ -0,0 +1,39 @@ +/* + Copyright 2012-2024 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.functions.string; + +import com.ezylang.evalex.EvaluationException; +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.functions.AbstractFunction; +import com.ezylang.evalex.functions.FunctionParameter; +import com.ezylang.evalex.parser.Token; + +/** + * Returns the given string with all leading and trailing space removed. + * + * @author LeonardoSoaresDev + */ +@FunctionParameter(name = "string") +public class StringTrimFunction extends AbstractFunction { + + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) + throws EvaluationException { + return expression.convertValue(parameterValues[0].getStringValue().trim()); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/string/StringUpperFunction.java b/src/main/java/com/ezylang/evalex/functions/string/StringUpperFunction.java new file mode 100644 index 000000000..e777f1c2d --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/string/StringUpperFunction.java @@ -0,0 +1,35 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.functions.string; + +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.functions.AbstractFunction; +import com.ezylang.evalex.functions.FunctionParameter; +import com.ezylang.evalex.parser.Token; + +/** + * Converts the given value to upper case. + */ +@FunctionParameter(name = "value") +public class StringUpperFunction extends AbstractFunction { + + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + return expression.convertValue(parameterValues[0].getStringValue().toUpperCase()); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/trigonometric/AcosFunction.java b/src/main/java/com/ezylang/evalex/functions/trigonometric/AcosFunction.java new file mode 100644 index 000000000..bb37fa273 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/trigonometric/AcosFunction.java @@ -0,0 +1,52 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.functions.trigonometric; + +import com.ezylang.evalex.EvaluationException; +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.functions.AbstractFunction; +import com.ezylang.evalex.functions.FunctionParameter; +import com.ezylang.evalex.parser.Token; + +import java.math.BigDecimal; + +import static java.math.BigDecimal.ONE; + +/** + * Returns the arc-cosine (in degrees). + */ +@FunctionParameter(name = "value") +public class AcosFunction extends AbstractFunction { + + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) + throws EvaluationException { + + BigDecimal parameterValue = parameterValues[0].getNumberValue(); + + if (parameterValue.compareTo(ONE) > 0) { + throw new EvaluationException( + functionToken, "Illegal acos(x) for x > 1: x = " + parameterValue); + } + if (parameterValue.compareTo(MINUS_ONE) < 0) { + throw new EvaluationException( + functionToken, "Illegal acos(x) for x < -1: x = " + parameterValue); + } + return expression.convertDoubleValue(Math.toDegrees(Math.acos(parameterValue.doubleValue()))); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/trigonometric/AcosHFunction.java b/src/main/java/com/ezylang/evalex/functions/trigonometric/AcosHFunction.java new file mode 100644 index 000000000..6a1c6c993 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/trigonometric/AcosHFunction.java @@ -0,0 +1,43 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.functions.trigonometric; + +import com.ezylang.evalex.EvaluationException; +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.functions.AbstractFunction; +import com.ezylang.evalex.functions.FunctionParameter; +import com.ezylang.evalex.parser.Token; + +/** + * Returns the hyperbolic arc-cosine. + */ +@FunctionParameter(name = "value") +public class AcosHFunction extends AbstractFunction { + + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) + throws EvaluationException { + + /* Formula: acosh(x) = ln(x + sqrt(x^2 - 1)) */ + double value = parameterValues[0].getNumberValue().doubleValue(); + if (Double.compare(value, 1) < 0) { + throw new EvaluationException(functionToken, "Value must be greater or equal to one"); + } + return expression.convertDoubleValue(Math.log(value + (Math.sqrt(Math.pow(value, 2) - 1)))); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/trigonometric/AcosRFunction.java b/src/main/java/com/ezylang/evalex/functions/trigonometric/AcosRFunction.java new file mode 100644 index 000000000..37fc70e3a --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/trigonometric/AcosRFunction.java @@ -0,0 +1,53 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.functions.trigonometric; + +import com.ezylang.evalex.EvaluationException; +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.functions.AbstractFunction; +import com.ezylang.evalex.functions.FunctionParameter; +import com.ezylang.evalex.parser.Token; + +import java.math.BigDecimal; + +import static java.math.BigDecimal.ONE; + +/** + * Returns the arc-cosine (in radians). + */ +@FunctionParameter(name = "cosine") +public class AcosRFunction extends AbstractFunction { + + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) + throws EvaluationException { + + BigDecimal parameterValue = parameterValues[0].getNumberValue(); + + if (parameterValue.compareTo(ONE) > 0) { + throw new EvaluationException( + functionToken, "Illegal acosr(x) for x > 1: x = " + parameterValue); + } + if (parameterValue.compareTo(MINUS_ONE) < 0) { + throw new EvaluationException( + functionToken, "Illegal acosr(x) for x < -1: x = " + parameterValue); + } + + return expression.convertDoubleValue(Math.acos(parameterValue.doubleValue())); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/trigonometric/AcotFunction.java b/src/main/java/com/ezylang/evalex/functions/trigonometric/AcotFunction.java new file mode 100644 index 000000000..b0153d328 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/trigonometric/AcotFunction.java @@ -0,0 +1,39 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.functions.trigonometric; + +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.functions.AbstractFunction; +import com.ezylang.evalex.functions.FunctionParameter; +import com.ezylang.evalex.parser.Token; + +/** + * Returns the arc-co-tangent (in degrees). + */ +@FunctionParameter(name = "value", nonZero = true) +public class AcotFunction extends AbstractFunction { + + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + + /* Formula: acot(x) = (pi / 2) - atan(x) */ + return expression.convertDoubleValue( + Math.toDegrees( + (Math.PI / 2) - Math.atan(parameterValues[0].getNumberValue().doubleValue()))); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/trigonometric/AcotHFunction.java b/src/main/java/com/ezylang/evalex/functions/trigonometric/AcotHFunction.java new file mode 100644 index 000000000..e5def354d --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/trigonometric/AcotHFunction.java @@ -0,0 +1,38 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.functions.trigonometric; + +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.functions.AbstractFunction; +import com.ezylang.evalex.functions.FunctionParameter; +import com.ezylang.evalex.parser.Token; + +/** + * Returns the arc hyperbolic cotangent. + */ +@FunctionParameter(name = "value") +public class AcotHFunction extends AbstractFunction { + + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + + /* Formula: acoth(x) = log((x + 1) / (x - 1)) * 0.5 */ + double value = parameterValues[0].getNumberValue().doubleValue(); + return expression.convertDoubleValue(Math.log((value + 1) / (value - 1)) * 0.5); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/trigonometric/AcotRFunction.java b/src/main/java/com/ezylang/evalex/functions/trigonometric/AcotRFunction.java new file mode 100644 index 000000000..32a004c3d --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/trigonometric/AcotRFunction.java @@ -0,0 +1,38 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.functions.trigonometric; + +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.functions.AbstractFunction; +import com.ezylang.evalex.functions.FunctionParameter; +import com.ezylang.evalex.parser.Token; + +/** + * Returns the arc-co-tangent (in radians). + */ +@FunctionParameter(name = "value", nonZero = true) +public class AcotRFunction extends AbstractFunction { + + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + + /* Formula: acot(x) = (pi / 2) - atan(x) */ + return expression.convertDoubleValue( + (Math.PI / 2) - Math.atan(parameterValues[0].getNumberValue().doubleValue())); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/trigonometric/AsinFunction.java b/src/main/java/com/ezylang/evalex/functions/trigonometric/AsinFunction.java new file mode 100644 index 000000000..63cd79e7f --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/trigonometric/AsinFunction.java @@ -0,0 +1,52 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.functions.trigonometric; + +import com.ezylang.evalex.EvaluationException; +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.functions.AbstractFunction; +import com.ezylang.evalex.functions.FunctionParameter; +import com.ezylang.evalex.parser.Token; + +import java.math.BigDecimal; + +import static java.math.BigDecimal.ONE; + +/** + * Returns the arc-sine (in degrees). + */ +@FunctionParameter(name = "value") +public class AsinFunction extends AbstractFunction { + + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) + throws EvaluationException { + + BigDecimal parameterValue = parameterValues[0].getNumberValue(); + + if (parameterValue.compareTo(ONE) > 0) { + throw new EvaluationException( + functionToken, "Illegal asin(x) for x > 1: x = " + parameterValue); + } + if (parameterValue.compareTo(MINUS_ONE) < 0) { + throw new EvaluationException( + functionToken, "Illegal asin(x) for x < -1: x = " + parameterValue); + } + return expression.convertDoubleValue(Math.toDegrees(Math.asin(parameterValue.doubleValue()))); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/trigonometric/AsinHFunction.java b/src/main/java/com/ezylang/evalex/functions/trigonometric/AsinHFunction.java new file mode 100644 index 000000000..5555c09b9 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/trigonometric/AsinHFunction.java @@ -0,0 +1,38 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.functions.trigonometric; + +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.functions.AbstractFunction; +import com.ezylang.evalex.functions.FunctionParameter; +import com.ezylang.evalex.parser.Token; + +/** + * Returns the hyperbolic arc-sine. + */ +@FunctionParameter(name = "value") +public class AsinHFunction extends AbstractFunction { + + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + + /* Formula: asinh(x) = ln(x + sqrt(x^2 + 1)) */ + double value = parameterValues[0].getNumberValue().doubleValue(); + return expression.convertDoubleValue(Math.log(value + (Math.sqrt(Math.pow(value, 2) + 1)))); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/trigonometric/AsinRFunction.java b/src/main/java/com/ezylang/evalex/functions/trigonometric/AsinRFunction.java new file mode 100644 index 000000000..8fdfccda0 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/trigonometric/AsinRFunction.java @@ -0,0 +1,56 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.functions.trigonometric; + +import com.ezylang.evalex.EvaluationException; +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.functions.AbstractFunction; +import com.ezylang.evalex.functions.FunctionParameter; +import com.ezylang.evalex.parser.Token; + +import java.math.BigDecimal; + +import static java.math.BigDecimal.ONE; +import static java.math.BigDecimal.valueOf; + +/** + * Returns the arc-sine (in radians). + */ +@FunctionParameter(name = "value") +public class AsinRFunction extends AbstractFunction { + + private static final BigDecimal MINUS_ONE = valueOf(-1); + + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) + throws EvaluationException { + + BigDecimal parameterValue = parameterValues[0].getNumberValue(); + + if (parameterValue.compareTo(ONE) > 0) { + throw new EvaluationException( + functionToken, "Illegal asinr(x) for x > 1: x = " + parameterValue); + } + if (parameterValue.compareTo(MINUS_ONE) < 0) { + throw new EvaluationException( + functionToken, "Illegal asinr(x) for x < -1: x = " + parameterValue); + } + return expression.convertDoubleValue( + Math.asin(parameterValues[0].getNumberValue().doubleValue())); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/trigonometric/Atan2Function.java b/src/main/java/com/ezylang/evalex/functions/trigonometric/Atan2Function.java new file mode 100644 index 000000000..43f1529d1 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/trigonometric/Atan2Function.java @@ -0,0 +1,41 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.functions.trigonometric; + +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.functions.AbstractFunction; +import com.ezylang.evalex.functions.FunctionParameter; +import com.ezylang.evalex.parser.Token; + +/** + * Returns the angle of atan2 (in degrees). + */ +@FunctionParameter(name = "y") +@FunctionParameter(name = "x") +public class Atan2Function extends AbstractFunction { + + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + + return expression.convertDoubleValue( + Math.toDegrees( + Math.atan2( + parameterValues[0].getNumberValue().doubleValue(), + parameterValues[1].getNumberValue().doubleValue()))); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/trigonometric/Atan2RFunction.java b/src/main/java/com/ezylang/evalex/functions/trigonometric/Atan2RFunction.java new file mode 100644 index 000000000..6b17758c9 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/trigonometric/Atan2RFunction.java @@ -0,0 +1,40 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.functions.trigonometric; + +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.functions.AbstractFunction; +import com.ezylang.evalex.functions.FunctionParameter; +import com.ezylang.evalex.parser.Token; + +/** + * Returns the angle of atan2 (in radians). + */ +@FunctionParameter(name = "y") +@FunctionParameter(name = "x") +public class Atan2RFunction extends AbstractFunction { + + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + + return expression.convertDoubleValue( + Math.atan2( + parameterValues[0].getNumberValue().doubleValue(), + parameterValues[1].getNumberValue().doubleValue())); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/trigonometric/AtanFunction.java b/src/main/java/com/ezylang/evalex/functions/trigonometric/AtanFunction.java new file mode 100644 index 000000000..0a4ee7ca1 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/trigonometric/AtanFunction.java @@ -0,0 +1,37 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.functions.trigonometric; + +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.functions.AbstractFunction; +import com.ezylang.evalex.functions.FunctionParameter; +import com.ezylang.evalex.parser.Token; + +/** + * Returns the arc-tangent (in degrees). + */ +@FunctionParameter(name = "value") +public class AtanFunction extends AbstractFunction { + + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + + return expression.convertDoubleValue( + Math.toDegrees(Math.atan(parameterValues[0].getNumberValue().doubleValue()))); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/trigonometric/AtanHFunction.java b/src/main/java/com/ezylang/evalex/functions/trigonometric/AtanHFunction.java new file mode 100644 index 000000000..344733235 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/trigonometric/AtanHFunction.java @@ -0,0 +1,43 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.functions.trigonometric; + +import com.ezylang.evalex.EvaluationException; +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.functions.AbstractFunction; +import com.ezylang.evalex.functions.FunctionParameter; +import com.ezylang.evalex.parser.Token; + +/** + * Returns the hyperbolic arc-sine. + */ +@FunctionParameter(name = "value") +public class AtanHFunction extends AbstractFunction { + + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) + throws EvaluationException { + + /* Formula: atanh(x) = 0.5*ln((1 + x)/(1 - x)) */ + double value = parameterValues[0].getNumberValue().doubleValue(); + if (Math.abs(value) >= 1) { + throw new EvaluationException(functionToken, "Absolute value must be less than 1"); + } + return expression.convertDoubleValue(0.5 * Math.log((1 + value) / (1 - value))); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/trigonometric/AtanRFunction.java b/src/main/java/com/ezylang/evalex/functions/trigonometric/AtanRFunction.java new file mode 100644 index 000000000..67927c54a --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/trigonometric/AtanRFunction.java @@ -0,0 +1,37 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.functions.trigonometric; + +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.functions.AbstractFunction; +import com.ezylang.evalex.functions.FunctionParameter; +import com.ezylang.evalex.parser.Token; + +/** + * Returns the arc-tangent (in radians). + */ +@FunctionParameter(name = "value") +public class AtanRFunction extends AbstractFunction { + + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + + return expression.convertDoubleValue( + Math.atan(parameterValues[0].getNumberValue().doubleValue())); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/trigonometric/CosFunction.java b/src/main/java/com/ezylang/evalex/functions/trigonometric/CosFunction.java new file mode 100644 index 000000000..7282f55c0 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/trigonometric/CosFunction.java @@ -0,0 +1,37 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.functions.trigonometric; + +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.functions.AbstractFunction; +import com.ezylang.evalex.functions.FunctionParameter; +import com.ezylang.evalex.parser.Token; + +/** + * Returns the trigonometric cosine of an angle (in degrees). + */ +@FunctionParameter(name = "value") +public class CosFunction extends AbstractFunction { + + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + + return expression.convertDoubleValue( + Math.cos(Math.toRadians(parameterValues[0].getNumberValue().doubleValue()))); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/trigonometric/CosHFunction.java b/src/main/java/com/ezylang/evalex/functions/trigonometric/CosHFunction.java new file mode 100644 index 000000000..ac0176fc5 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/trigonometric/CosHFunction.java @@ -0,0 +1,37 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.functions.trigonometric; + +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.functions.AbstractFunction; +import com.ezylang.evalex.functions.FunctionParameter; +import com.ezylang.evalex.parser.Token; + +/** + * Returns the hyperbolic cosine of a value. + */ +@FunctionParameter(name = "value") +public class CosHFunction extends AbstractFunction { + + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + + return expression.convertDoubleValue( + Math.cosh(parameterValues[0].getNumberValue().doubleValue())); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/trigonometric/CosRFunction.java b/src/main/java/com/ezylang/evalex/functions/trigonometric/CosRFunction.java new file mode 100644 index 000000000..6cf5916f9 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/trigonometric/CosRFunction.java @@ -0,0 +1,37 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.functions.trigonometric; + +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.functions.AbstractFunction; +import com.ezylang.evalex.functions.FunctionParameter; +import com.ezylang.evalex.parser.Token; + +/** + * Returns the trigonometric cosine of an angle (in radians). + */ +@FunctionParameter(name = "value") +public class CosRFunction extends AbstractFunction { + + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + + return expression.convertDoubleValue( + Math.cos(parameterValues[0].getNumberValue().doubleValue())); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/trigonometric/CotFunction.java b/src/main/java/com/ezylang/evalex/functions/trigonometric/CotFunction.java new file mode 100644 index 000000000..a75697242 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/trigonometric/CotFunction.java @@ -0,0 +1,38 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.functions.trigonometric; + +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.functions.AbstractFunction; +import com.ezylang.evalex.functions.FunctionParameter; +import com.ezylang.evalex.parser.Token; + +/** + * Returns the co-tangent of an angle (in degrees). + */ +@FunctionParameter(name = "value", nonZero = true) +public class CotFunction extends AbstractFunction { + + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + + /* Formula: cot(x) = cos(x) / sin(x) = 1 / tan(x) */ + return expression.convertDoubleValue( + 1 / Math.tan(Math.toRadians(parameterValues[0].getNumberValue().doubleValue()))); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/trigonometric/CotHFunction.java b/src/main/java/com/ezylang/evalex/functions/trigonometric/CotHFunction.java new file mode 100644 index 000000000..269ba570c --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/trigonometric/CotHFunction.java @@ -0,0 +1,38 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.functions.trigonometric; + +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.functions.AbstractFunction; +import com.ezylang.evalex.functions.FunctionParameter; +import com.ezylang.evalex.parser.Token; + +/** + * Returns the hyperbolic co-tangent of a value. + */ +@FunctionParameter(name = "value", nonZero = true) +public class CotHFunction extends AbstractFunction { + + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + + /* Formula: coth(x) = 1 / tanh(x) */ + return expression.convertDoubleValue( + 1 / Math.tanh(parameterValues[0].getNumberValue().doubleValue())); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/trigonometric/CotRFunction.java b/src/main/java/com/ezylang/evalex/functions/trigonometric/CotRFunction.java new file mode 100644 index 000000000..b63619558 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/trigonometric/CotRFunction.java @@ -0,0 +1,38 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.functions.trigonometric; + +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.functions.AbstractFunction; +import com.ezylang.evalex.functions.FunctionParameter; +import com.ezylang.evalex.parser.Token; + +/** + * Returns the trigonometric co-tangent of an angle (in radians). + */ +@FunctionParameter(name = "value", nonZero = true) +public class CotRFunction extends AbstractFunction { + + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + + /* Formula: cot(x) = cos(x) / sin(x) = 1 / tan(x) */ + return expression.convertDoubleValue( + 1 / Math.tan(parameterValues[0].getNumberValue().doubleValue())); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/trigonometric/CscFunction.java b/src/main/java/com/ezylang/evalex/functions/trigonometric/CscFunction.java new file mode 100644 index 000000000..ffcef00c5 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/trigonometric/CscFunction.java @@ -0,0 +1,38 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.functions.trigonometric; + +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.functions.AbstractFunction; +import com.ezylang.evalex.functions.FunctionParameter; +import com.ezylang.evalex.parser.Token; + +/** + * Returns the co-secant (in degrees). + */ +@FunctionParameter(name = "value", nonZero = true) +public class CscFunction extends AbstractFunction { + + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + + /* Formula: csc(x) = 1 / sin(x) */ + return expression.convertDoubleValue( + 1 / Math.sin(Math.toRadians(parameterValues[0].getNumberValue().doubleValue()))); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/trigonometric/CscHFunction.java b/src/main/java/com/ezylang/evalex/functions/trigonometric/CscHFunction.java new file mode 100644 index 000000000..3f5ece892 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/trigonometric/CscHFunction.java @@ -0,0 +1,38 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.functions.trigonometric; + +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.functions.AbstractFunction; +import com.ezylang.evalex.functions.FunctionParameter; +import com.ezylang.evalex.parser.Token; + +/** + * Returns the co-secant. + */ +@FunctionParameter(name = "value", nonZero = true) +public class CscHFunction extends AbstractFunction { + + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + + /* Formula: csch(x) = 1 / sinh(x) */ + return expression.convertDoubleValue( + 1 / Math.sinh(parameterValues[0].getNumberValue().doubleValue())); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/trigonometric/CscRFunction.java b/src/main/java/com/ezylang/evalex/functions/trigonometric/CscRFunction.java new file mode 100644 index 000000000..efeb3df57 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/trigonometric/CscRFunction.java @@ -0,0 +1,38 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.functions.trigonometric; + +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.functions.AbstractFunction; +import com.ezylang.evalex.functions.FunctionParameter; +import com.ezylang.evalex.parser.Token; + +/** + * Returns the co-secant (in radians). + */ +@FunctionParameter(name = "value", nonZero = true) +public class CscRFunction extends AbstractFunction { + + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + + /* Formula: csc(x) = 1 / sin(x) */ + return expression.convertDoubleValue( + 1 / Math.sin(parameterValues[0].getNumberValue().doubleValue())); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/trigonometric/DegFunction.java b/src/main/java/com/ezylang/evalex/functions/trigonometric/DegFunction.java new file mode 100644 index 000000000..52eecd888 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/trigonometric/DegFunction.java @@ -0,0 +1,38 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.functions.trigonometric; + +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.functions.AbstractFunction; +import com.ezylang.evalex.functions.FunctionParameter; +import com.ezylang.evalex.parser.Token; + +/** + * Converts an angle measured in radians to an approximately equivalent angle measured in degrees. + */ +@FunctionParameter(name = "radians") +public class DegFunction extends AbstractFunction { + + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + + double rad = Math.toDegrees(parameterValues[0].getNumberValue().doubleValue()); + + return expression.convertDoubleValue(rad); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/trigonometric/RadFunction.java b/src/main/java/com/ezylang/evalex/functions/trigonometric/RadFunction.java new file mode 100644 index 000000000..a0fb6ee94 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/trigonometric/RadFunction.java @@ -0,0 +1,38 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.functions.trigonometric; + +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.functions.AbstractFunction; +import com.ezylang.evalex.functions.FunctionParameter; +import com.ezylang.evalex.parser.Token; + +/** + * Converts an angle measured in degrees to an approximately equivalent angle measured in radians. + */ +@FunctionParameter(name = "degrees") +public class RadFunction extends AbstractFunction { + + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + + double deg = Math.toRadians(parameterValues[0].getNumberValue().doubleValue()); + + return expression.convertDoubleValue(deg); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/trigonometric/SecFunction.java b/src/main/java/com/ezylang/evalex/functions/trigonometric/SecFunction.java new file mode 100644 index 000000000..1c3035413 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/trigonometric/SecFunction.java @@ -0,0 +1,38 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.functions.trigonometric; + +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.functions.AbstractFunction; +import com.ezylang.evalex.functions.FunctionParameter; +import com.ezylang.evalex.parser.Token; + +/** + * Returns the secant (in degrees). + */ +@FunctionParameter(name = "value", nonZero = true) +public class SecFunction extends AbstractFunction { + + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + + /* Formula: sec(x) = 1 / cos(x) */ + return expression.convertDoubleValue( + 1 / Math.cos(Math.toRadians(parameterValues[0].getNumberValue().doubleValue()))); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/trigonometric/SecHFunction.java b/src/main/java/com/ezylang/evalex/functions/trigonometric/SecHFunction.java new file mode 100644 index 000000000..8f452fa11 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/trigonometric/SecHFunction.java @@ -0,0 +1,38 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.functions.trigonometric; + +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.functions.AbstractFunction; +import com.ezylang.evalex.functions.FunctionParameter; +import com.ezylang.evalex.parser.Token; + +/** + * Returns the hyperbolic secant. + */ +@FunctionParameter(name = "value", nonZero = true) +public class SecHFunction extends AbstractFunction { + + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + + /* Formula: sech(x) = 1 / cosh(x) */ + return expression.convertDoubleValue( + 1 / Math.cosh(parameterValues[0].getNumberValue().doubleValue())); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/trigonometric/SecRFunction.java b/src/main/java/com/ezylang/evalex/functions/trigonometric/SecRFunction.java new file mode 100644 index 000000000..950d8dc04 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/trigonometric/SecRFunction.java @@ -0,0 +1,38 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.functions.trigonometric; + +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.functions.AbstractFunction; +import com.ezylang.evalex.functions.FunctionParameter; +import com.ezylang.evalex.parser.Token; + +/** + * Returns the secant (in radians). + */ +@FunctionParameter(name = "value", nonZero = true) +public class SecRFunction extends AbstractFunction { + + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + + /* Formula: sec(x) = 1 / cos(x) */ + return expression.convertDoubleValue( + 1 / Math.cos(parameterValues[0].getNumberValue().doubleValue())); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/trigonometric/SinFunction.java b/src/main/java/com/ezylang/evalex/functions/trigonometric/SinFunction.java new file mode 100644 index 000000000..7390426d8 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/trigonometric/SinFunction.java @@ -0,0 +1,37 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.functions.trigonometric; + +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.functions.AbstractFunction; +import com.ezylang.evalex.functions.FunctionParameter; +import com.ezylang.evalex.parser.Token; + +/** + * Returns the trigonometric sine of an angle (in degrees). + */ +@FunctionParameter(name = "value") +public class SinFunction extends AbstractFunction { + + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + + return expression.convertDoubleValue( + Math.sin(Math.toRadians(parameterValues[0].getNumberValue().doubleValue()))); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/trigonometric/SinHFunction.java b/src/main/java/com/ezylang/evalex/functions/trigonometric/SinHFunction.java new file mode 100644 index 000000000..911501538 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/trigonometric/SinHFunction.java @@ -0,0 +1,37 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.functions.trigonometric; + +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.functions.AbstractFunction; +import com.ezylang.evalex.functions.FunctionParameter; +import com.ezylang.evalex.parser.Token; + +/** + * Returns the hyperbolic sine of a value. + */ +@FunctionParameter(name = "value") +public class SinHFunction extends AbstractFunction { + + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + + return expression.convertDoubleValue( + Math.sinh(parameterValues[0].getNumberValue().doubleValue())); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/trigonometric/SinRFunction.java b/src/main/java/com/ezylang/evalex/functions/trigonometric/SinRFunction.java new file mode 100644 index 000000000..b5b017095 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/trigonometric/SinRFunction.java @@ -0,0 +1,37 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.functions.trigonometric; + +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.functions.AbstractFunction; +import com.ezylang.evalex.functions.FunctionParameter; +import com.ezylang.evalex.parser.Token; + +/** + * Returns the trigonometric sine of an angle (in radians). + */ +@FunctionParameter(name = "value") +public class SinRFunction extends AbstractFunction { + + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + + return expression.convertDoubleValue( + Math.sin(parameterValues[0].getNumberValue().doubleValue())); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/trigonometric/TanFunction.java b/src/main/java/com/ezylang/evalex/functions/trigonometric/TanFunction.java new file mode 100644 index 000000000..994efefec --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/trigonometric/TanFunction.java @@ -0,0 +1,37 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.functions.trigonometric; + +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.functions.AbstractFunction; +import com.ezylang.evalex.functions.FunctionParameter; +import com.ezylang.evalex.parser.Token; + +/** + * Returns the trigonometric tangent of an angle (in degrees). + */ +@FunctionParameter(name = "value") +public class TanFunction extends AbstractFunction { + + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + + return expression.convertDoubleValue( + Math.tan(Math.toRadians(parameterValues[0].getNumberValue().doubleValue()))); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/trigonometric/TanHFunction.java b/src/main/java/com/ezylang/evalex/functions/trigonometric/TanHFunction.java new file mode 100644 index 000000000..aad6232c8 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/trigonometric/TanHFunction.java @@ -0,0 +1,37 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.functions.trigonometric; + +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.functions.AbstractFunction; +import com.ezylang.evalex.functions.FunctionParameter; +import com.ezylang.evalex.parser.Token; + +/** + * Returns the hyperbolic tangent of a value. + */ +@FunctionParameter(name = "value") +public class TanHFunction extends AbstractFunction { + + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + + return expression.convertDoubleValue( + Math.tanh(parameterValues[0].getNumberValue().doubleValue())); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/trigonometric/TanRFunction.java b/src/main/java/com/ezylang/evalex/functions/trigonometric/TanRFunction.java new file mode 100644 index 000000000..e82a4d85e --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/trigonometric/TanRFunction.java @@ -0,0 +1,37 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.functions.trigonometric; + +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.functions.AbstractFunction; +import com.ezylang.evalex.functions.FunctionParameter; +import com.ezylang.evalex.parser.Token; + +/** + * Returns the trigonometric tangent of an angle (in radians). + */ +@FunctionParameter(name = "value") +public class TanRFunction extends AbstractFunction { + + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + + return expression.convertDoubleValue( + Math.tan(parameterValues[0].getNumberValue().doubleValue())); + } +} diff --git a/src/main/java/com/ezylang/evalex/operators/AbstractOperator.java b/src/main/java/com/ezylang/evalex/operators/AbstractOperator.java new file mode 100644 index 000000000..1aeaad18c --- /dev/null +++ b/src/main/java/com/ezylang/evalex/operators/AbstractOperator.java @@ -0,0 +1,94 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.operators; + +import com.ezylang.evalex.config.ExpressionConfiguration; +import lombok.Getter; + +import static com.ezylang.evalex.operators.OperatorIfc.OperatorType.PREFIX_OPERATOR; + +/** + * Abstract implementation of the {@link OperatorIfc}, used as base class for operator + * implementations. + */ +public abstract class AbstractOperator implements OperatorIfc { + + @Getter private final int precedence; + + private final boolean leftAssociative; + + private final boolean operandsLazy; + + OperatorType type; + + /** + * Creates a new operator and uses the {@link InfixOperator} annotation to create the operator + * definition. + */ + protected AbstractOperator() { + InfixOperator infixAnnotation = getClass().getAnnotation(InfixOperator.class); + PrefixOperator prefixAnnotation = getClass().getAnnotation(PrefixOperator.class); + PostfixOperator postfixAnnotation = getClass().getAnnotation(PostfixOperator.class); + if (infixAnnotation != null) { + this.type = OperatorType.INFIX_OPERATOR; + this.precedence = infixAnnotation.precedence(); + this.leftAssociative = infixAnnotation.leftAssociative(); + this.operandsLazy = infixAnnotation.operandsLazy(); + } else if (prefixAnnotation != null) { + this.type = PREFIX_OPERATOR; + this.precedence = prefixAnnotation.precedence(); + this.leftAssociative = prefixAnnotation.leftAssociative(); + this.operandsLazy = false; + } else if (postfixAnnotation != null) { + this.type = OperatorType.POSTFIX_OPERATOR; + this.precedence = postfixAnnotation.precedence(); + this.leftAssociative = postfixAnnotation.leftAssociative(); + this.operandsLazy = false; + } else { + throw new OperatorAnnotationNotFoundException(this.getClass().getName()); + } + } + + @Override + public int getPrecedence(ExpressionConfiguration configuration) { + return getPrecedence(); + } + + @Override + public boolean isLeftAssociative() { + return leftAssociative; + } + + @Override + public boolean isOperandLazy() { + return operandsLazy; + } + + @Override + public boolean isPrefix() { + return type == PREFIX_OPERATOR; + } + + @Override + public boolean isPostfix() { + return type == OperatorType.POSTFIX_OPERATOR; + } + + @Override + public boolean isInfix() { + return type == OperatorType.INFIX_OPERATOR; + } +} diff --git a/src/main/java/com/ezylang/evalex/operators/InfixOperator.java b/src/main/java/com/ezylang/evalex/operators/InfixOperator.java new file mode 100644 index 000000000..261c4f34b --- /dev/null +++ b/src/main/java/com/ezylang/evalex/operators/InfixOperator.java @@ -0,0 +1,46 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.operators; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * The infix operator annotation + */ +@Documented +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface InfixOperator { + + /** + * Operator precedence, usually one from the constants in {@link OperatorIfc}. + */ + int precedence(); + + /** + * Operator associativity, defaults to true. + */ + boolean leftAssociative() default true; + + /** + * Operands are evaluated lazily, defaults to false. + */ + boolean operandsLazy() default false; +} diff --git a/src/main/java/com/ezylang/evalex/operators/OperatorAnnotationNotFoundException.java b/src/main/java/com/ezylang/evalex/operators/OperatorAnnotationNotFoundException.java new file mode 100644 index 000000000..b241cff07 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/operators/OperatorAnnotationNotFoundException.java @@ -0,0 +1,27 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.operators; + +/** + * Operator properties are defined through a class annotation, this exception is thrown if no + * annotation was found when creating the operator instance. + */ +public class OperatorAnnotationNotFoundException extends RuntimeException { + + public OperatorAnnotationNotFoundException(String className) { + super("Operator annotation for '" + className + "' not found"); + } +} diff --git a/src/main/java/com/ezylang/evalex/operators/OperatorIfc.java b/src/main/java/com/ezylang/evalex/operators/OperatorIfc.java new file mode 100644 index 000000000..ba1289c3b --- /dev/null +++ b/src/main/java/com/ezylang/evalex/operators/OperatorIfc.java @@ -0,0 +1,157 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.operators; + +import com.ezylang.evalex.EvaluationException; +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.config.ExpressionConfiguration; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.parser.Token; + +/** + * Interface that is required for all operators in an operator dictionary for evaluation of + * expressions. There are three operator type: prefix, postfix and infix. Every operator has a + * precedence, which defines the order of operator evaluation. The associativity of an operator is a + * property that determines how operators of the same precedence are grouped in the absence of + * parentheses. + */ +public interface OperatorIfc { + + /** + * The operator type. + */ + enum OperatorType { + /** + * Unary prefix operator, like -x + */ + PREFIX_OPERATOR, + /** + * Unary postfix operator,like x! + */ + POSTFIX_OPERATOR, + /** + * Binary infix operator, like x+y + */ + INFIX_OPERATOR + } + + /** + * Or operator precedence: || + */ + int OPERATOR_PRECEDENCE_OR = 2; + + /** + * And operator precedence: && + */ + int OPERATOR_PRECEDENCE_AND = 4; + + /** + * Equality operators precedence: =, ==, !=, <> + */ + int OPERATOR_PRECEDENCE_EQUALITY = 7; + + /** + * Comparative operators precedence: <, >, <=, >= + */ + int OPERATOR_PRECEDENCE_COMPARISON = 10; + + /** + * Additive operators precedence: + and - + */ + int OPERATOR_PRECEDENCE_ADDITIVE = 20; + + /** + * Multiplicative operators precedence: *, /, % + */ + int OPERATOR_PRECEDENCE_MULTIPLICATIVE = 30; + + /** + * Power operator precedence: ^ + */ + int OPERATOR_PRECEDENCE_POWER = 40; + + /** + * Unary operators precedence: + and - as prefix + */ + int OPERATOR_PRECEDENCE_UNARY = 60; + + /** + * An optional higher power operator precedence, higher than the unary prefix, e.g. -2^2 equals to + * 4 or -4, depending on precedence configuration. + */ + int OPERATOR_PRECEDENCE_POWER_HIGHER = 80; + + /** + * @return The operator's precedence. + */ + int getPrecedence(); + + /** + * If operators with same precedence are evaluated from left to right. + * + * @return The associativity. + */ + boolean isLeftAssociative(); + + /** + * If it is a prefix operator. + * + * @return true if it is a prefix operator. + */ + boolean isPrefix(); + + /** + * If it is a postfix operator. + * + * @return true if it is a postfix operator. + */ + boolean isPostfix(); + + /** + * If it is an infix operator. + * + * @return true if it is an infix operator. + */ + boolean isInfix(); + + /** + * Called during parsing, can be implemented to return a customized precedence. + * + * @param configuration The expression configuration. + * @return The default precedence from the operator annotation, or a customized value. + */ + int getPrecedence(ExpressionConfiguration configuration); + + /** + * Checks if the operand is lazy. + * + * @return true if operands are defined as lazy. + */ + boolean isOperandLazy(); + + /** + * Performs the operator logic and returns an evaluation result. + * + * @param expression The expression, where this function is executed. Can be used to access the + * expression configuration. + * @param operatorToken The operator token from the parsed expression. + * @param operands The operands, one for prefix and postfix operators, two for infix operators. + * @return The evaluation result in form of a {@link EvaluationValue}. + * @throws EvaluationException In case there were problems during evaluation. + */ + EvaluationValue evaluate(Expression expression, Token operatorToken, EvaluationValue... operands) + throws EvaluationException; +} diff --git a/src/main/java/com/ezylang/evalex/operators/PostfixOperator.java b/src/main/java/com/ezylang/evalex/operators/PostfixOperator.java new file mode 100644 index 000000000..12619335b --- /dev/null +++ b/src/main/java/com/ezylang/evalex/operators/PostfixOperator.java @@ -0,0 +1,43 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.operators; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import static com.ezylang.evalex.operators.OperatorIfc.OPERATOR_PRECEDENCE_UNARY; + +/** + * The postfix operator annotation + */ +@Documented +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface PostfixOperator { + + /** + * Operator precedence, usually one from the constants in {@link OperatorIfc}. + */ + int precedence() default OPERATOR_PRECEDENCE_UNARY; + + /** + * Operator associativity, defaults to true. + */ + boolean leftAssociative() default true; +} diff --git a/src/main/java/com/ezylang/evalex/operators/PrefixOperator.java b/src/main/java/com/ezylang/evalex/operators/PrefixOperator.java new file mode 100644 index 000000000..41d2c296d --- /dev/null +++ b/src/main/java/com/ezylang/evalex/operators/PrefixOperator.java @@ -0,0 +1,43 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.operators; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import static com.ezylang.evalex.operators.OperatorIfc.OPERATOR_PRECEDENCE_UNARY; + +/** + * The prefix operator annotation + */ +@Documented +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface PrefixOperator { + + /** + * Operator precedence, usually one from the constants in {@link OperatorIfc}. + */ + int precedence() default OPERATOR_PRECEDENCE_UNARY; + + /** + * Operator associativity, defaults to true. + */ + boolean leftAssociative() default true; +} diff --git a/src/main/java/com/ezylang/evalex/operators/arithmetic/InfixDivisionOperator.java b/src/main/java/com/ezylang/evalex/operators/arithmetic/InfixDivisionOperator.java new file mode 100644 index 000000000..dee428702 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/operators/arithmetic/InfixDivisionOperator.java @@ -0,0 +1,57 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.operators.arithmetic; + +import com.ezylang.evalex.EvaluationException; +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.operators.AbstractOperator; +import com.ezylang.evalex.operators.InfixOperator; +import com.ezylang.evalex.parser.Token; + +import java.math.BigDecimal; + +import static com.ezylang.evalex.operators.OperatorIfc.OPERATOR_PRECEDENCE_MULTIPLICATIVE; + +/** + * Division of two numbers. + */ +@InfixOperator(precedence = OPERATOR_PRECEDENCE_MULTIPLICATIVE) +public class InfixDivisionOperator extends AbstractOperator { + + @Override + public EvaluationValue evaluate( + Expression expression, Token operatorToken, EvaluationValue... operands) + throws EvaluationException { + EvaluationValue leftOperand = operands[0]; + EvaluationValue rightOperand = operands[1]; + + if (leftOperand.isNumberValue() && rightOperand.isNumberValue()) { + + if (rightOperand.getNumberValue().equals(BigDecimal.ZERO)) { + throw new EvaluationException(operatorToken, "Division by zero"); + } + + return expression.convertValue( + leftOperand + .getNumberValue() + .divide( + rightOperand.getNumberValue(), expression.getConfiguration().getMathContext())); + } else { + throw EvaluationException.ofUnsupportedDataTypeInOperation(operatorToken); + } + } +} diff --git a/src/main/java/com/ezylang/evalex/operators/arithmetic/InfixMinusOperator.java b/src/main/java/com/ezylang/evalex/operators/arithmetic/InfixMinusOperator.java new file mode 100644 index 000000000..8a1769030 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/operators/arithmetic/InfixMinusOperator.java @@ -0,0 +1,70 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.operators.arithmetic; + +import com.ezylang.evalex.EvaluationException; +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.operators.AbstractOperator; +import com.ezylang.evalex.operators.InfixOperator; +import com.ezylang.evalex.parser.Token; + +import java.time.Duration; + +import static com.ezylang.evalex.operators.OperatorIfc.OPERATOR_PRECEDENCE_ADDITIVE; + +/** + * Subtraction of two numbers. + */ +@InfixOperator(precedence = OPERATOR_PRECEDENCE_ADDITIVE) +public class InfixMinusOperator extends AbstractOperator { + + @Override + public EvaluationValue evaluate( + Expression expression, Token operatorToken, EvaluationValue... operands) + throws EvaluationException { + EvaluationValue leftOperand = operands[0]; + EvaluationValue rightOperand = operands[1]; + + if (leftOperand.isNumberValue() && rightOperand.isNumberValue()) { + return expression.convertValue( + leftOperand + .getNumberValue() + .subtract( + rightOperand.getNumberValue(), expression.getConfiguration().getMathContext())); + + } else if (leftOperand.isDateTimeValue() && rightOperand.isDateTimeValue()) { + return expression.convertValue( + Duration.ofMillis( + leftOperand.getDateTimeValue().toEpochMilli() + - rightOperand.getDateTimeValue().toEpochMilli())); + + } else if (leftOperand.isDateTimeValue() && rightOperand.isDurationValue()) { + return expression.convertValue( + leftOperand.getDateTimeValue().minus(rightOperand.getDurationValue())); + } else if (leftOperand.isDurationValue() && rightOperand.isDurationValue()) { + return expression.convertValue( + leftOperand.getDurationValue().minus(rightOperand.getDurationValue())); + } else if (leftOperand.isDateTimeValue() && rightOperand.isNumberValue()) { + return expression.convertValue( + leftOperand + .getDateTimeValue() + .minus(Duration.ofMillis(rightOperand.getNumberValue().longValue()))); + } else { + throw EvaluationException.ofUnsupportedDataTypeInOperation(operatorToken); + } + } +} diff --git a/src/main/java/com/ezylang/evalex/operators/arithmetic/InfixModuloOperator.java b/src/main/java/com/ezylang/evalex/operators/arithmetic/InfixModuloOperator.java new file mode 100644 index 000000000..b3e85df8d --- /dev/null +++ b/src/main/java/com/ezylang/evalex/operators/arithmetic/InfixModuloOperator.java @@ -0,0 +1,57 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.operators.arithmetic; + +import com.ezylang.evalex.EvaluationException; +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.operators.AbstractOperator; +import com.ezylang.evalex.operators.InfixOperator; +import com.ezylang.evalex.parser.Token; + +import java.math.BigDecimal; + +import static com.ezylang.evalex.operators.OperatorIfc.OPERATOR_PRECEDENCE_MULTIPLICATIVE; + +/** + * Remainder (modulo) of two numbers. + */ +@InfixOperator(precedence = OPERATOR_PRECEDENCE_MULTIPLICATIVE) +public class InfixModuloOperator extends AbstractOperator { + + @Override + public EvaluationValue evaluate( + Expression expression, Token operatorToken, EvaluationValue... operands) + throws EvaluationException { + EvaluationValue leftOperand = operands[0]; + EvaluationValue rightOperand = operands[1]; + + if (leftOperand.isNumberValue() && rightOperand.isNumberValue()) { + + if (rightOperand.getNumberValue().equals(BigDecimal.ZERO)) { + throw new EvaluationException(operatorToken, "Division by zero"); + } + + return expression.convertValue( + leftOperand + .getNumberValue() + .remainder( + rightOperand.getNumberValue(), expression.getConfiguration().getMathContext())); + } else { + throw EvaluationException.ofUnsupportedDataTypeInOperation(operatorToken); + } + } +} diff --git a/src/main/java/com/ezylang/evalex/operators/arithmetic/InfixMultiplicationOperator.java b/src/main/java/com/ezylang/evalex/operators/arithmetic/InfixMultiplicationOperator.java new file mode 100644 index 000000000..ed4675c69 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/operators/arithmetic/InfixMultiplicationOperator.java @@ -0,0 +1,50 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.operators.arithmetic; + +import com.ezylang.evalex.EvaluationException; +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.operators.AbstractOperator; +import com.ezylang.evalex.operators.InfixOperator; +import com.ezylang.evalex.parser.Token; + +import static com.ezylang.evalex.operators.OperatorIfc.OPERATOR_PRECEDENCE_MULTIPLICATIVE; + +/** + * Multiplication of two numbers. + */ +@InfixOperator(precedence = OPERATOR_PRECEDENCE_MULTIPLICATIVE) +public class InfixMultiplicationOperator extends AbstractOperator { + + @Override + public EvaluationValue evaluate( + Expression expression, Token operatorToken, EvaluationValue... operands) + throws EvaluationException { + EvaluationValue leftOperand = operands[0]; + EvaluationValue rightOperand = operands[1]; + + if (leftOperand.isNumberValue() && rightOperand.isNumberValue()) { + return expression.convertValue( + leftOperand + .getNumberValue() + .multiply( + rightOperand.getNumberValue(), expression.getConfiguration().getMathContext())); + } else { + throw EvaluationException.ofUnsupportedDataTypeInOperation(operatorToken); + } + } +} diff --git a/src/main/java/com/ezylang/evalex/operators/arithmetic/InfixPlusOperator.java b/src/main/java/com/ezylang/evalex/operators/arithmetic/InfixPlusOperator.java new file mode 100644 index 000000000..e9cc974bb --- /dev/null +++ b/src/main/java/com/ezylang/evalex/operators/arithmetic/InfixPlusOperator.java @@ -0,0 +1,60 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.operators.arithmetic; + +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.operators.AbstractOperator; +import com.ezylang.evalex.operators.InfixOperator; +import com.ezylang.evalex.parser.Token; + +import java.time.Duration; + +import static com.ezylang.evalex.operators.OperatorIfc.OPERATOR_PRECEDENCE_ADDITIVE; + +/** + * Addition of numbers and strings. If one operand is a string, a string concatenation is performed. + */ +@InfixOperator(precedence = OPERATOR_PRECEDENCE_ADDITIVE) +public class InfixPlusOperator extends AbstractOperator { + + @Override + public EvaluationValue evaluate( + Expression expression, Token operatorToken, EvaluationValue... operands) { + EvaluationValue leftOperand = operands[0]; + EvaluationValue rightOperand = operands[1]; + + if (leftOperand.isNumberValue() && rightOperand.isNumberValue()) { + return expression.convertValue( + leftOperand + .getNumberValue() + .add(rightOperand.getNumberValue(), expression.getConfiguration().getMathContext())); + } else if (leftOperand.isDateTimeValue() && rightOperand.isDurationValue()) { + return expression.convertValue( + leftOperand.getDateTimeValue().plus(rightOperand.getDurationValue())); + } else if (leftOperand.isDurationValue() && rightOperand.isDurationValue()) { + return expression.convertValue( + leftOperand.getDurationValue().plus(rightOperand.getDurationValue())); + } else if (leftOperand.isDateTimeValue() && rightOperand.isNumberValue()) { + return expression.convertValue( + leftOperand + .getDateTimeValue() + .plus(Duration.ofMillis(rightOperand.getNumberValue().longValue()))); + } else { + return expression.convertValue(leftOperand.getStringValue() + rightOperand.getStringValue()); + } + } +} diff --git a/src/main/java/com/ezylang/evalex/operators/arithmetic/InfixPowerOfOperator.java b/src/main/java/com/ezylang/evalex/operators/arithmetic/InfixPowerOfOperator.java new file mode 100644 index 000000000..d7d768a40 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/operators/arithmetic/InfixPowerOfOperator.java @@ -0,0 +1,80 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.operators.arithmetic; + +import com.ezylang.evalex.EvaluationException; +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.config.ExpressionConfiguration; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.operators.AbstractOperator; +import com.ezylang.evalex.operators.InfixOperator; +import com.ezylang.evalex.parser.Token; + +import java.math.BigDecimal; +import java.math.MathContext; +import java.math.RoundingMode; + +import static com.ezylang.evalex.operators.OperatorIfc.OPERATOR_PRECEDENCE_POWER; + +/** + * Power of operator, calculates the power of right operand of left operand. The precedence is read + * from the configuration during parsing. + * + * @see #getPrecedence(ExpressionConfiguration) + */ +@InfixOperator(precedence = OPERATOR_PRECEDENCE_POWER, leftAssociative = false) +public class InfixPowerOfOperator extends AbstractOperator { + + @Override + public EvaluationValue evaluate( + Expression expression, Token operatorToken, EvaluationValue... operands) + throws EvaluationException { + EvaluationValue leftOperand = operands[0]; + EvaluationValue rightOperand = operands[1]; + + if (leftOperand.isNumberValue() && rightOperand.isNumberValue()) { + /*- + * Thanks to Gene Marin: + * http://stackoverflow.com/questions/3579779/how-to-do-a-fractional-power-on-bigdecimal-in-java + */ + + MathContext mathContext = expression.getConfiguration().getMathContext(); + BigDecimal v1 = leftOperand.getNumberValue(); + BigDecimal v2 = rightOperand.getNumberValue(); + + int signOf2 = v2.signum(); + double dn1 = v1.doubleValue(); + v2 = v2.multiply(new BigDecimal(signOf2)); // n2 is now positive + BigDecimal remainderOf2 = v2.remainder(BigDecimal.ONE); + BigDecimal n2IntPart = v2.subtract(remainderOf2); + BigDecimal intPow = v1.pow(n2IntPart.intValueExact(), mathContext); + BigDecimal doublePow = BigDecimal.valueOf(Math.pow(dn1, remainderOf2.doubleValue())); + + BigDecimal result = intPow.multiply(doublePow, mathContext); + if (signOf2 == -1) { + result = BigDecimal.ONE.divide(result, mathContext.getPrecision(), RoundingMode.HALF_UP); + } + return expression.convertValue(result); + } else { + throw EvaluationException.ofUnsupportedDataTypeInOperation(operatorToken); + } + } + + @Override + public int getPrecedence(ExpressionConfiguration configuration) { + return configuration.getPowerOfPrecedence(); + } +} diff --git a/src/main/java/com/ezylang/evalex/operators/arithmetic/PrefixMinusOperator.java b/src/main/java/com/ezylang/evalex/operators/arithmetic/PrefixMinusOperator.java new file mode 100644 index 000000000..18edbb79e --- /dev/null +++ b/src/main/java/com/ezylang/evalex/operators/arithmetic/PrefixMinusOperator.java @@ -0,0 +1,44 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.operators.arithmetic; + +import com.ezylang.evalex.EvaluationException; +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.operators.AbstractOperator; +import com.ezylang.evalex.operators.PrefixOperator; +import com.ezylang.evalex.parser.Token; + +/** + * Unary prefix minus. + */ +@PrefixOperator(leftAssociative = false) +public class PrefixMinusOperator extends AbstractOperator { + + @Override + public EvaluationValue evaluate( + Expression expression, Token operatorToken, EvaluationValue... operands) + throws EvaluationException { + EvaluationValue operand = operands[0]; + + if (operand.isNumberValue()) { + return expression.convertValue( + operand.getNumberValue().negate(expression.getConfiguration().getMathContext())); + } else { + throw EvaluationException.ofUnsupportedDataTypeInOperation(operatorToken); + } + } +} diff --git a/src/main/java/com/ezylang/evalex/operators/arithmetic/PrefixPlusOperator.java b/src/main/java/com/ezylang/evalex/operators/arithmetic/PrefixPlusOperator.java new file mode 100644 index 000000000..0534afc76 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/operators/arithmetic/PrefixPlusOperator.java @@ -0,0 +1,44 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.operators.arithmetic; + +import com.ezylang.evalex.EvaluationException; +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.operators.AbstractOperator; +import com.ezylang.evalex.operators.PrefixOperator; +import com.ezylang.evalex.parser.Token; + +/** + * Unary prefix plus. + */ +@PrefixOperator(leftAssociative = false) +public class PrefixPlusOperator extends AbstractOperator { + + @Override + public EvaluationValue evaluate( + Expression expression, Token operatorToken, EvaluationValue... operands) + throws EvaluationException { + EvaluationValue operator = operands[0]; + + if (operator.isNumberValue()) { + return expression.convertValue( + operator.getNumberValue().plus(expression.getConfiguration().getMathContext())); + } else { + throw EvaluationException.ofUnsupportedDataTypeInOperation(operatorToken); + } + } +} diff --git a/src/main/java/com/ezylang/evalex/operators/booleans/InfixAndOperator.java b/src/main/java/com/ezylang/evalex/operators/booleans/InfixAndOperator.java new file mode 100644 index 000000000..7369df47d --- /dev/null +++ b/src/main/java/com/ezylang/evalex/operators/booleans/InfixAndOperator.java @@ -0,0 +1,41 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.operators.booleans; + +import com.ezylang.evalex.EvaluationException; +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.operators.AbstractOperator; +import com.ezylang.evalex.operators.InfixOperator; +import com.ezylang.evalex.parser.Token; + +import static com.ezylang.evalex.operators.OperatorIfc.OPERATOR_PRECEDENCE_AND; + +/** + * Boolean AND of two values. + */ +@InfixOperator(precedence = OPERATOR_PRECEDENCE_AND, operandsLazy = true) +public class InfixAndOperator extends AbstractOperator { + + @Override + public EvaluationValue evaluate( + Expression expression, Token operatorToken, EvaluationValue... operands) + throws EvaluationException { + return expression.convertValue( + expression.evaluateSubtree(operands[0].getExpressionNode()).getBooleanValue() + && expression.evaluateSubtree(operands[1].getExpressionNode()).getBooleanValue()); + } +} diff --git a/src/main/java/com/ezylang/evalex/operators/booleans/InfixEqualsOperator.java b/src/main/java/com/ezylang/evalex/operators/booleans/InfixEqualsOperator.java new file mode 100644 index 000000000..3943958d4 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/operators/booleans/InfixEqualsOperator.java @@ -0,0 +1,43 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.operators.booleans; + +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.operators.AbstractOperator; +import com.ezylang.evalex.operators.InfixOperator; +import com.ezylang.evalex.parser.Token; + +import static com.ezylang.evalex.operators.OperatorIfc.OPERATOR_PRECEDENCE_EQUALITY; + +/** + * Equality of two values. + */ +@InfixOperator(precedence = OPERATOR_PRECEDENCE_EQUALITY) +public class InfixEqualsOperator extends AbstractOperator { + + @Override + public EvaluationValue evaluate( + Expression expression, Token operatorToken, EvaluationValue... operands) { + if (operands[0].getDataType() != operands[1].getDataType()) { + return EvaluationValue.FALSE; + } + if (operands[0].isNullValue() && operands[1].isNullValue()) { + return EvaluationValue.TRUE; + } + return expression.convertValue(operands[0].compareTo(operands[1]) == 0); + } +} diff --git a/src/main/java/com/ezylang/evalex/operators/booleans/InfixGreaterEqualsOperator.java b/src/main/java/com/ezylang/evalex/operators/booleans/InfixGreaterEqualsOperator.java new file mode 100644 index 000000000..98efb053a --- /dev/null +++ b/src/main/java/com/ezylang/evalex/operators/booleans/InfixGreaterEqualsOperator.java @@ -0,0 +1,37 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.operators.booleans; + +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.operators.AbstractOperator; +import com.ezylang.evalex.operators.InfixOperator; +import com.ezylang.evalex.parser.Token; + +import static com.ezylang.evalex.operators.OperatorIfc.OPERATOR_PRECEDENCE_COMPARISON; + +/** + * Greater or equals of two values. + */ +@InfixOperator(precedence = OPERATOR_PRECEDENCE_COMPARISON) +public class InfixGreaterEqualsOperator extends AbstractOperator { + + @Override + public EvaluationValue evaluate( + Expression expression, Token operatorToken, EvaluationValue... operands) { + return expression.convertValue(operands[0].compareTo(operands[1]) >= 0); + } +} diff --git a/src/main/java/com/ezylang/evalex/operators/booleans/InfixGreaterOperator.java b/src/main/java/com/ezylang/evalex/operators/booleans/InfixGreaterOperator.java new file mode 100644 index 000000000..b35da474f --- /dev/null +++ b/src/main/java/com/ezylang/evalex/operators/booleans/InfixGreaterOperator.java @@ -0,0 +1,37 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.operators.booleans; + +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.operators.AbstractOperator; +import com.ezylang.evalex.operators.InfixOperator; +import com.ezylang.evalex.parser.Token; + +import static com.ezylang.evalex.operators.OperatorIfc.OPERATOR_PRECEDENCE_COMPARISON; + +/** + * Greater of two values. + */ +@InfixOperator(precedence = OPERATOR_PRECEDENCE_COMPARISON) +public class InfixGreaterOperator extends AbstractOperator { + + @Override + public EvaluationValue evaluate( + Expression expression, Token operatorToken, EvaluationValue... operands) { + return expression.convertValue(operands[0].compareTo(operands[1]) > 0); + } +} diff --git a/src/main/java/com/ezylang/evalex/operators/booleans/InfixLessEqualsOperator.java b/src/main/java/com/ezylang/evalex/operators/booleans/InfixLessEqualsOperator.java new file mode 100644 index 000000000..993f94b46 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/operators/booleans/InfixLessEqualsOperator.java @@ -0,0 +1,37 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.operators.booleans; + +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.operators.AbstractOperator; +import com.ezylang.evalex.operators.InfixOperator; +import com.ezylang.evalex.parser.Token; + +import static com.ezylang.evalex.operators.OperatorIfc.OPERATOR_PRECEDENCE_COMPARISON; + +/** + * Less or equals of two values. + */ +@InfixOperator(precedence = OPERATOR_PRECEDENCE_COMPARISON) +public class InfixLessEqualsOperator extends AbstractOperator { + + @Override + public EvaluationValue evaluate( + Expression expression, Token operatorToken, EvaluationValue... operands) { + return expression.convertValue(operands[0].compareTo(operands[1]) <= 0); + } +} diff --git a/src/main/java/com/ezylang/evalex/operators/booleans/InfixLessOperator.java b/src/main/java/com/ezylang/evalex/operators/booleans/InfixLessOperator.java new file mode 100644 index 000000000..e77e4b8bc --- /dev/null +++ b/src/main/java/com/ezylang/evalex/operators/booleans/InfixLessOperator.java @@ -0,0 +1,37 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.operators.booleans; + +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.operators.AbstractOperator; +import com.ezylang.evalex.operators.InfixOperator; +import com.ezylang.evalex.parser.Token; + +import static com.ezylang.evalex.operators.OperatorIfc.OPERATOR_PRECEDENCE_COMPARISON; + +/** + * Less of two values. + */ +@InfixOperator(precedence = OPERATOR_PRECEDENCE_COMPARISON) +public class InfixLessOperator extends AbstractOperator { + + @Override + public EvaluationValue evaluate( + Expression expression, Token operatorToken, EvaluationValue... operands) { + return expression.convertValue(operands[0].compareTo(operands[1]) < 0); + } +} diff --git a/src/main/java/com/ezylang/evalex/operators/booleans/InfixNotEqualsOperator.java b/src/main/java/com/ezylang/evalex/operators/booleans/InfixNotEqualsOperator.java new file mode 100644 index 000000000..9d0a9e054 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/operators/booleans/InfixNotEqualsOperator.java @@ -0,0 +1,43 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.operators.booleans; + +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.operators.AbstractOperator; +import com.ezylang.evalex.operators.InfixOperator; +import com.ezylang.evalex.parser.Token; + +import static com.ezylang.evalex.operators.OperatorIfc.OPERATOR_PRECEDENCE_EQUALITY; + +/** + * No equality of two values. + */ +@InfixOperator(precedence = OPERATOR_PRECEDENCE_EQUALITY) +public class InfixNotEqualsOperator extends AbstractOperator { + + @Override + public EvaluationValue evaluate( + Expression expression, Token operatorToken, EvaluationValue... operands) { + if (operands[0].getDataType() != operands[1].getDataType()) { + return EvaluationValue.TRUE; + } + if (operands[0].isNullValue() && operands[1].isNullValue()) { + return EvaluationValue.FALSE; + } + return expression.convertValue(operands[0].compareTo(operands[1]) != 0); + } +} diff --git a/src/main/java/com/ezylang/evalex/operators/booleans/InfixOrOperator.java b/src/main/java/com/ezylang/evalex/operators/booleans/InfixOrOperator.java new file mode 100644 index 000000000..cd6b58370 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/operators/booleans/InfixOrOperator.java @@ -0,0 +1,41 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.operators.booleans; + +import com.ezylang.evalex.EvaluationException; +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.operators.AbstractOperator; +import com.ezylang.evalex.operators.InfixOperator; +import com.ezylang.evalex.parser.Token; + +import static com.ezylang.evalex.operators.OperatorIfc.OPERATOR_PRECEDENCE_OR; + +/** + * Boolean OR of two values. + */ +@InfixOperator(precedence = OPERATOR_PRECEDENCE_OR, operandsLazy = true) +public class InfixOrOperator extends AbstractOperator { + + @Override + public EvaluationValue evaluate( + Expression expression, Token operatorToken, EvaluationValue... operands) + throws EvaluationException { + return expression.convertValue( + expression.evaluateSubtree(operands[0].getExpressionNode()).getBooleanValue() + || expression.evaluateSubtree(operands[1].getExpressionNode()).getBooleanValue()); + } +} diff --git a/src/main/java/com/ezylang/evalex/operators/booleans/PrefixNotOperator.java b/src/main/java/com/ezylang/evalex/operators/booleans/PrefixNotOperator.java new file mode 100644 index 000000000..8aa990e76 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/operators/booleans/PrefixNotOperator.java @@ -0,0 +1,35 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.operators.booleans; + +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.operators.AbstractOperator; +import com.ezylang.evalex.operators.PrefixOperator; +import com.ezylang.evalex.parser.Token; + +/** + * Boolean negation of value. + */ +@PrefixOperator +public class PrefixNotOperator extends AbstractOperator { + + @Override + public EvaluationValue evaluate( + Expression expression, Token operatorToken, EvaluationValue... operands) { + return expression.convertValue(!operands[0].getBooleanValue()); + } +} diff --git a/src/main/java/com/ezylang/evalex/parser/ASTNode.java b/src/main/java/com/ezylang/evalex/parser/ASTNode.java new file mode 100644 index 000000000..f1ccf417e --- /dev/null +++ b/src/main/java/com/ezylang/evalex/parser/ASTNode.java @@ -0,0 +1,73 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.parser; + +import lombok.Value; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +/** + * Expressions are parsed into an abstract syntax tree (AST). The tree has one root node and each + * node has zero or more children (parameters), depending on the operation. A leaf node is a + * numerical or string constant that has no more children (parameters). Other nodes define + * operators, functions and special operations like array index and structure separation. + * + *

The tree is evaluated from bottom (leafs) to top, in a recursive way, until the root node is + * evaluated, which then holds the result of the complete expression. + * + *

To be able to visualize the tree, a toJSON method is provided. The produced JSON + * string can be used to visualize the tree. OE.g. with this online tool: + * + *

Online JSON to Tree Diagram Converter + */ +@Value +public class ASTNode { + + /** + * The children od the tree. + */ + List parameters; + + /** + * The token associated with this tree node. + */ + Token token; + + public ASTNode(Token token, ASTNode... parameters) { + this.token = token; + this.parameters = Arrays.asList(parameters); + } + + /** + * Produces a JSON string representation of this node ad all its children. + * + * @return A JSON string of the tree structure starting at this node. + */ + public String toJSON() { + if (parameters.isEmpty()) { + return String.format( + "{" + "\"type\":\"%s\",\"value\":\"%s\"}", token.getType(), token.getValue()); + } else { + String childrenJson = + parameters.stream().map(ASTNode::toJSON).collect(Collectors.joining(",")); + return String.format( + "{" + "\"type\":\"%s\",\"value\":\"%s\",\"children\":[%s]}", + token.getType(), token.getValue(), childrenJson); + } + } +} diff --git a/src/main/java/com/ezylang/evalex/parser/ParseException.java b/src/main/java/com/ezylang/evalex/parser/ParseException.java new file mode 100644 index 000000000..f7ff219a4 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/parser/ParseException.java @@ -0,0 +1,42 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.parser; + +import com.ezylang.evalex.BaseException; +import lombok.ToString; + +/** + * Exception while parsing the expression. + */ +@ToString(callSuper = true) +public class ParseException extends BaseException { + + public ParseException(int startPosition, int endPosition, String tokenString, String message) { + super(startPosition, endPosition, tokenString, message); + } + + public ParseException(String expression, String message) { + super(1, expression.length(), expression, message); + } + + public ParseException(Token token, String message) { + super( + token.getStartPosition(), + token.getStartPosition() + token.getValue().length() - 1, + token.getValue(), + message); + } +} diff --git a/src/main/java/com/ezylang/evalex/parser/ShuntingYardConverter.java b/src/main/java/com/ezylang/evalex/parser/ShuntingYardConverter.java new file mode 100644 index 000000000..0ceac8ddf --- /dev/null +++ b/src/main/java/com/ezylang/evalex/parser/ShuntingYardConverter.java @@ -0,0 +1,292 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.parser; + +import com.ezylang.evalex.config.ExpressionConfiguration; +import com.ezylang.evalex.functions.FunctionIfc; +import com.ezylang.evalex.operators.OperatorIfc; +import com.ezylang.evalex.parser.Token.TokenType; + +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Deque; +import java.util.List; + +import static com.ezylang.evalex.parser.Token.TokenType.ARRAY_INDEX; +import static com.ezylang.evalex.parser.Token.TokenType.ARRAY_OPEN; +import static com.ezylang.evalex.parser.Token.TokenType.BRACE_OPEN; +import static com.ezylang.evalex.parser.Token.TokenType.FUNCTION; +import static com.ezylang.evalex.parser.Token.TokenType.STRUCTURE_SEPARATOR; + +/** + * The shunting yard algorithm can be used to convert a mathematical expression from an infix + * notation into either a postfix notation (RPN, reverse polish notation), or into an abstract + * syntax tree (AST). + * + *

Here it is used to parse and convert a list of already parsed expression tokens into an AST. + * + * @see Shunting yard algorithm + * @see Abstract syntax tree + */ +public class ShuntingYardConverter { + + private final List expressionTokens; + + private final String originalExpression; + + private final ExpressionConfiguration configuration; + + private final Deque operatorStack = new ArrayDeque<>(); + private final Deque operandStack = new ArrayDeque<>(); + + public ShuntingYardConverter( + String originalExpression, + List expressionTokens, + ExpressionConfiguration configuration) { + this.originalExpression = originalExpression; + this.expressionTokens = expressionTokens; + this.configuration = configuration; + } + + public ASTNode toAbstractSyntaxTree() throws ParseException { + + Token previousToken = null; + for (Token currentToken : expressionTokens) { + switch (currentToken.getType()) { + case VARIABLE_OR_CONSTANT: + case NUMBER_LITERAL: + case STRING_LITERAL: + operandStack.push(new ASTNode(currentToken)); + break; + case FUNCTION: + operatorStack.push(currentToken); + break; + case COMMA: + processOperatorsFromStackUntilTokenType(BRACE_OPEN); + break; + case INFIX_OPERATOR: + case PREFIX_OPERATOR: + case POSTFIX_OPERATOR: + processOperator(currentToken); + break; + case BRACE_OPEN: + processBraceOpen(previousToken, currentToken); + break; + case BRACE_CLOSE: + processBraceClose(); + break; + case ARRAY_OPEN: + processArrayOpen(currentToken); + break; + case ARRAY_CLOSE: + processArrayClose(); + break; + case STRUCTURE_SEPARATOR: + processStructureSeparator(currentToken); + break; + default: + throw new ParseException( + currentToken, "Unexpected token of type '" + currentToken.getType() + "'"); + } + previousToken = currentToken; + } + + while (!operatorStack.isEmpty()) { + Token token = operatorStack.pop(); + createOperatorNode(token); + } + + if (operandStack.isEmpty()) { + throw new ParseException(this.originalExpression, "Empty expression"); + } + + if (operandStack.size() > 1) { + throw new ParseException(this.originalExpression, "Too many operands"); + } + + return operandStack.pop(); + } + + private void processStructureSeparator(Token currentToken) throws ParseException { + Token nextToken = operatorStack.isEmpty() ? null : operatorStack.peek(); + while (nextToken != null && nextToken.getType() == STRUCTURE_SEPARATOR) { + Token token = operatorStack.pop(); + createOperatorNode(token); + nextToken = operatorStack.peek(); + } + operatorStack.push(currentToken); + } + + private void processBraceOpen(Token previousToken, Token currentToken) { + if (previousToken != null && previousToken.getType() == FUNCTION) { + // start of parameter list, marker for variable number of arguments + Token paramStart = + new Token( + currentToken.getStartPosition(), + currentToken.getValue(), + TokenType.FUNCTION_PARAM_START); + operandStack.push(new ASTNode(paramStart)); + } + operatorStack.push(currentToken); + } + + private void processBraceClose() throws ParseException { + processOperatorsFromStackUntilTokenType(BRACE_OPEN); + operatorStack.pop(); // throw away the marker + if (!operatorStack.isEmpty() && operatorStack.peek().getType() == FUNCTION) { + Token functionToken = operatorStack.pop(); + ArrayList parameters = new ArrayList<>(); + while (true) { + // add all parameters in reverse order from stack to the parameter array + ASTNode node = operandStack.pop(); + if (node.getToken().getType() == TokenType.FUNCTION_PARAM_START) { + break; + } + parameters.add(0, node); + } + validateFunctionParameters(functionToken, parameters); + operandStack.push(new ASTNode(functionToken, parameters.toArray(new ASTNode[0]))); + } + } + + private void validateFunctionParameters(Token functionToken, ArrayList parameters) + throws ParseException { + FunctionIfc function = functionToken.getFunctionDefinition(); + if (parameters.size() < function.getCountOfNonVarArgParameters()) { + throw new ParseException(functionToken, "Not enough parameters for function"); + } + if (!function.hasVarArgs() + && parameters.size() > function.getFunctionParameterDefinitions().size()) { + throw new ParseException(functionToken, "Too many parameters for function"); + } + } + + /** + * Array index is treated like a function with two parameters. First parameter is the array (name + * or evaluation result). Second parameter is the array index. + * + * @param currentToken The current ARRAY_OPEN ("[") token. + */ + private void processArrayOpen(Token currentToken) throws ParseException { + Token nextToken = operatorStack.isEmpty() ? null : operatorStack.peek(); + while (nextToken != null && (nextToken.getType() == STRUCTURE_SEPARATOR)) { + Token token = operatorStack.pop(); + createOperatorNode(token); + nextToken = operatorStack.isEmpty() ? null : operatorStack.peek(); + } + // create ARRAY_INDEX operator (just like a function name) and push it to the operator stack + Token arrayIndex = + new Token(currentToken.getStartPosition(), currentToken.getValue(), ARRAY_INDEX); + operatorStack.push(arrayIndex); + + // push the ARRAY_OPEN to the operators, too (to later match the ARRAY_CLOSE) + operatorStack.push(currentToken); + } + + /** + * Follows the logic for a function, but with two fixed parameters. + * + * @throws ParseException If there were problems while processing the stacks. + */ + private void processArrayClose() throws ParseException { + processOperatorsFromStackUntilTokenType(ARRAY_OPEN); + operatorStack.pop(); // throw away the marker + Token arrayToken = operatorStack.pop(); + ArrayList operands = new ArrayList<>(); + + // second parameter of the "ARRAY_INDEX" function is the index (first on stack) + ASTNode index = operandStack.pop(); + operands.add(0, index); + + // first parameter of the "ARRAY_INDEX" function is the array (name or evaluation result) + // (second on stack) + ASTNode array = operandStack.pop(); + operands.add(0, array); + + operandStack.push(new ASTNode(arrayToken, operands.toArray(new ASTNode[0]))); + } + + private void processOperatorsFromStackUntilTokenType(TokenType untilTokenType) + throws ParseException { + while (!operatorStack.isEmpty() && operatorStack.peek().getType() != untilTokenType) { + Token token = operatorStack.pop(); + createOperatorNode(token); + } + } + + private void createOperatorNode(Token token) throws ParseException { + if (operandStack.isEmpty()) { + throw new ParseException(token, "Missing operand for operator"); + } + + ASTNode operand1 = operandStack.pop(); + + if (token.getType() == TokenType.PREFIX_OPERATOR + || token.getType() == TokenType.POSTFIX_OPERATOR) { + operandStack.push(new ASTNode(token, operand1)); + } else { + if (operandStack.isEmpty()) { + throw new ParseException(token, "Missing second operand for operator"); + } + ASTNode operand2 = operandStack.pop(); + operandStack.push(new ASTNode(token, operand2, operand1)); + } + } + + private void processOperator(Token currentToken) throws ParseException { + Token nextToken = operatorStack.isEmpty() ? null : operatorStack.peek(); + while (isOperator(nextToken) + && isNextOperatorOfHigherPrecedence( + currentToken.getOperatorDefinition(), nextToken.getOperatorDefinition())) { + Token token = operatorStack.pop(); + createOperatorNode(token); + nextToken = operatorStack.isEmpty() ? null : operatorStack.peek(); + } + operatorStack.push(currentToken); + } + + private boolean isNextOperatorOfHigherPrecedence( + OperatorIfc currentOperator, OperatorIfc nextOperator) { + // structure operator (null) has always a higher precedence than other operators + if (nextOperator == null) { + return true; + } + + if (currentOperator.isLeftAssociative()) { + return currentOperator.getPrecedence(configuration) + <= nextOperator.getPrecedence(configuration); + } else { + return currentOperator.getPrecedence(configuration) + < nextOperator.getPrecedence(configuration); + } + } + + private boolean isOperator(Token token) { + if (token == null) { + return false; + } + TokenType tokenType = token.getType(); + switch (tokenType) { + case INFIX_OPERATOR: + case PREFIX_OPERATOR: + case POSTFIX_OPERATOR: + case STRUCTURE_SEPARATOR: + return true; + default: + return false; + } + } +} diff --git a/src/main/java/com/ezylang/evalex/parser/Token.java b/src/main/java/com/ezylang/evalex/parser/Token.java new file mode 100644 index 000000000..cf4b776d2 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/parser/Token.java @@ -0,0 +1,80 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.parser; + +import com.ezylang.evalex.functions.FunctionIfc; +import com.ezylang.evalex.operators.OperatorIfc; +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.ToString; +import lombok.Value; + +/** + * A token represents a single part of an expression, like an operator, number literal, or a brace. + * Each token has a unique type, a value (its representation) and a position (starting with 1) in + * the original expression string. + * + *

For operators and functions, the operator and function definition is also set during parsing. + */ +@Value +@AllArgsConstructor +@EqualsAndHashCode(doNotUseGetters = true) +public class Token { + + public enum TokenType { + BRACE_OPEN, + BRACE_CLOSE, + COMMA, + STRING_LITERAL, + NUMBER_LITERAL, + VARIABLE_OR_CONSTANT, + INFIX_OPERATOR, + PREFIX_OPERATOR, + POSTFIX_OPERATOR, + FUNCTION, + FUNCTION_PARAM_START, + ARRAY_OPEN, + ARRAY_CLOSE, + ARRAY_INDEX, + STRUCTURE_SEPARATOR + } + + int startPosition; + + String value; + + TokenType type; + + @EqualsAndHashCode.Exclude + @ToString.Exclude + FunctionIfc functionDefinition; + + @EqualsAndHashCode.Exclude + @ToString.Exclude + OperatorIfc operatorDefinition; + + public Token(int startPosition, String value, TokenType type) { + this(startPosition, value, type, null, null); + } + + public Token(int startPosition, String value, TokenType type, FunctionIfc functionDefinition) { + this(startPosition, value, type, functionDefinition, null); + } + + public Token(int startPosition, String value, TokenType type, OperatorIfc operatorDefinition) { + this(startPosition, value, type, null, operatorDefinition); + } +} diff --git a/src/main/java/com/ezylang/evalex/parser/Tokenizer.java b/src/main/java/com/ezylang/evalex/parser/Tokenizer.java new file mode 100644 index 000000000..b9d24e6a6 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/parser/Tokenizer.java @@ -0,0 +1,652 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.parser; + +import com.ezylang.evalex.config.ExpressionConfiguration; +import com.ezylang.evalex.config.FunctionDictionaryIfc; +import com.ezylang.evalex.config.OperatorDictionaryIfc; +import com.ezylang.evalex.functions.FunctionIfc; +import com.ezylang.evalex.operators.OperatorIfc; +import com.ezylang.evalex.parser.Token.TokenType; + +import java.util.ArrayList; +import java.util.List; + +import static com.ezylang.evalex.parser.Token.TokenType.BRACE_CLOSE; +import static com.ezylang.evalex.parser.Token.TokenType.BRACE_OPEN; +import static com.ezylang.evalex.parser.Token.TokenType.FUNCTION; +import static com.ezylang.evalex.parser.Token.TokenType.INFIX_OPERATOR; +import static com.ezylang.evalex.parser.Token.TokenType.NUMBER_LITERAL; +import static com.ezylang.evalex.parser.Token.TokenType.POSTFIX_OPERATOR; +import static com.ezylang.evalex.parser.Token.TokenType.STRUCTURE_SEPARATOR; +import static com.ezylang.evalex.parser.Token.TokenType.VARIABLE_OR_CONSTANT; + +/** + * The tokenizer is responsible to parse a string and return a list of tokens. The order of tokens + * will follow the infix expression notation, skipping any blank characters. + */ +public class Tokenizer { + + private final String expressionString; + + private final OperatorDictionaryIfc operatorDictionary; + + private final FunctionDictionaryIfc functionDictionary; + + private final ExpressionConfiguration configuration; + + private final List tokens = new ArrayList<>(); + + private int currentColumnIndex = 0; + + private int currentChar = -2; + + private int braceBalance; + + private int arrayBalance; + + public Tokenizer(String expressionString, ExpressionConfiguration configuration) { + this.expressionString = expressionString; + this.configuration = configuration; + this.operatorDictionary = configuration.getOperatorDictionary(); + this.functionDictionary = configuration.getFunctionDictionary(); + } + + /** + * Parse the given expression and return a list of tokens, representing the expression. + * + * @return A list of expression tokens. + * @throws ParseException When the expression can't be parsed. + */ + public List parse() throws ParseException { + Token currentToken = getNextToken(); + while (currentToken != null) { + if (implicitMultiplicationPossible(currentToken)) { + if (configuration.isImplicitMultiplicationAllowed()) { + Token multiplication = + new Token( + currentToken.getStartPosition(), + "*", + TokenType.INFIX_OPERATOR, + operatorDictionary.getInfixOperator("*")); + tokens.add(multiplication); + } else { + throw new ParseException(currentToken, "Missing operator"); + } + } + isInfixInsteadOfPostfix(currentToken); + validateToken(currentToken); + tokens.add(currentToken); + currentToken = getNextToken(); + } + + if (braceBalance > 0) { + throw new ParseException(expressionString, "Closing brace not found"); + } + + if (arrayBalance > 0) { + throw new ParseException(expressionString, "Closing array not found"); + } + + return tokens; + } + + private boolean implicitMultiplicationPossible(Token currentToken) { + Token previousToken = getPreviousToken(); + + if (previousToken == null) { + return false; + } + + return ((previousToken.getType() == BRACE_CLOSE && currentToken.getType() == BRACE_OPEN) + || (previousToken.getType() == NUMBER_LITERAL + && currentToken.getType() == VARIABLE_OR_CONSTANT) + || (previousToken.getType() == NUMBER_LITERAL && currentToken.getType() == FUNCTION) + || (previousToken.getType() == NUMBER_LITERAL && currentToken.getType() == BRACE_OPEN)); + } + + private void isInfixInsteadOfPostfix(Token currentToken) { + if (currentToken == null || invalidTokenAfterInfixOperator(currentToken)) return; + Token previousToken = getPreviousToken(); + if (previousToken == null || previousToken.getType() != POSTFIX_OPERATOR) return; + String opString = previousToken.getValue(); + if (operatorDictionary.hasInfixOperator(opString)) { + OperatorIfc op = operatorDictionary.getInfixOperator(opString); + setPreviousToken(new Token(previousToken.getStartPosition(), opString, INFIX_OPERATOR, op)); + } + } + + private void validateToken(Token currentToken) throws ParseException { + + if (currentToken.getType() == STRUCTURE_SEPARATOR && getPreviousToken() == null) { + throw new ParseException(currentToken, "Misplaced structure operator"); + } + + Token previousToken = getPreviousToken(); + if (previousToken != null + && previousToken.getType() == INFIX_OPERATOR + && invalidTokenAfterInfixOperator(currentToken)) { + throw new ParseException(currentToken, "Unexpected token after infix operator"); + } + } + + private boolean invalidTokenAfterInfixOperator(Token token) { + switch (token.getType()) { + case INFIX_OPERATOR: + case POSTFIX_OPERATOR: + case BRACE_CLOSE: + case ARRAY_CLOSE: + case COMMA: + return true; + default: + return false; + } + } + + private Token getNextToken() throws ParseException { + + // blanks are always skipped. + skipBlanks(); + + // end of input + if (currentChar == -1) { + return null; + } + + // we have a token start, identify and parse it + if (isAtStringLiteralStart()) { + return parseStringLiteral(); + } else if (currentChar == '(') { + return parseBraceOpen(); + } else if (currentChar == ')') { + return parseBraceClose(); + } else if (currentChar == '[' && configuration.isArraysAllowed()) { + return parseArrayOpen(); + } else if (currentChar == ']' && configuration.isArraysAllowed()) { + return parseArrayClose(); + } else if (currentChar == '.' + && !isNextCharNumberChar() + && configuration.isStructuresAllowed()) { + return parseStructureSeparator(); + } else if (currentChar == ',') { + Token token = new Token(currentColumnIndex, ",", TokenType.COMMA); + consumeChar(); + return token; + } else if (isAtIdentifierStart()) { + return parseIdentifier(); + } else if (isAtNumberStart()) { + return parseNumberLiteral(); + } else { + return parseOperator(); + } + } + + private Token parseStructureSeparator() throws ParseException { + Token token = new Token(currentColumnIndex, ".", TokenType.STRUCTURE_SEPARATOR); + if (arrayOpenOrStructureSeparatorNotAllowed()) { + throw new ParseException(token, "Structure separator not allowed here"); + } + consumeChar(); + return token; + } + + private Token parseArrayClose() throws ParseException { + Token token = new Token(currentColumnIndex, "]", TokenType.ARRAY_CLOSE); + if (!arrayCloseAllowed()) { + throw new ParseException(token, "Array close not allowed here"); + } + consumeChar(); + arrayBalance--; + if (arrayBalance < 0) { + throw new ParseException(token, "Unexpected closing array"); + } + return token; + } + + private Token parseArrayOpen() throws ParseException { + Token token = new Token(currentColumnIndex, "[", TokenType.ARRAY_OPEN); + if (arrayOpenOrStructureSeparatorNotAllowed()) { + throw new ParseException(token, "Array open not allowed here"); + } + consumeChar(); + arrayBalance++; + return token; + } + + private Token parseBraceClose() throws ParseException { + Token token = new Token(currentColumnIndex, ")", TokenType.BRACE_CLOSE); + consumeChar(); + braceBalance--; + if (braceBalance < 0) { + throw new ParseException(token, "Unexpected closing brace"); + } + return token; + } + + private Token parseBraceOpen() { + Token token = new Token(currentColumnIndex, "(", BRACE_OPEN); + consumeChar(); + braceBalance++; + return token; + } + + private Token getPreviousToken() { + return tokens.isEmpty() ? null : tokens.get(tokens.size() - 1); + } + + private void setPreviousToken(Token token) { + if (!tokens.isEmpty()) { + tokens.set(tokens.size() - 1, token); + } + } + + private Token parseOperator() throws ParseException { + int tokenStartIndex = currentColumnIndex; + StringBuilder tokenValue = new StringBuilder(); + boolean prefixAllowed = prefixOperatorAllowed(); + boolean postfixAllowed = postfixOperatorAllowed(); + boolean infixAllowed = infixOperatorAllowed(); + while (true) { + tokenValue.append((char) currentChar); + String tokenString = tokenValue.toString(); + String possibleNextOperator = tokenString + (char) peekNextChar(); + boolean possibleNextOperatorFound = + (prefixAllowed && operatorDictionary.hasPrefixOperator(possibleNextOperator)) + || (postfixAllowed + && operatorDictionary.hasPostfixOperator(possibleNextOperator)) + || (infixAllowed + && operatorDictionary.hasInfixOperator(possibleNextOperator)); + consumeChar(); + if (!possibleNextOperatorFound) { + break; + } + } + String tokenString = tokenValue.toString(); + if (prefixAllowed && operatorDictionary.hasPrefixOperator(tokenString)) { + OperatorIfc operator = operatorDictionary.getPrefixOperator(tokenString); + return new Token(tokenStartIndex, tokenString, TokenType.PREFIX_OPERATOR, operator); + } else if (postfixAllowed && operatorDictionary.hasPostfixOperator(tokenString)) { + OperatorIfc operator = operatorDictionary.getPostfixOperator(tokenString); + return new Token(tokenStartIndex, tokenString, TokenType.POSTFIX_OPERATOR, operator); + } else if (operatorDictionary.hasInfixOperator(tokenString)) { + OperatorIfc operator = operatorDictionary.getInfixOperator(tokenString); + return new Token(tokenStartIndex, tokenString, TokenType.INFIX_OPERATOR, operator); + } else if (tokenString.equals(".") && configuration.isStructuresAllowed()) { + return new Token(tokenStartIndex, tokenString, STRUCTURE_SEPARATOR); + } + throw new ParseException( + tokenStartIndex, + tokenStartIndex + tokenString.length() - 1, + tokenString, + "Undefined operator '" + tokenString + "'"); + } + + private boolean arrayOpenOrStructureSeparatorNotAllowed() { + Token previousToken = getPreviousToken(); + + if (previousToken == null) { + return true; + } + + switch (previousToken.getType()) { + case BRACE_CLOSE: + case VARIABLE_OR_CONSTANT: + case ARRAY_CLOSE: + case STRING_LITERAL: + return false; + default: + return true; + } + } + + private boolean arrayCloseAllowed() { + Token previousToken = getPreviousToken(); + + if (previousToken == null) { + return false; + } + + switch (previousToken.getType()) { + case BRACE_OPEN: + case INFIX_OPERATOR: + case PREFIX_OPERATOR: + case FUNCTION: + case COMMA: + case ARRAY_OPEN: + return false; + default: + return true; + } + } + + private boolean prefixOperatorAllowed() { + Token previousToken = getPreviousToken(); + + if (previousToken == null) { + return true; + } + + switch (previousToken.getType()) { + case BRACE_OPEN: + case INFIX_OPERATOR: + case COMMA: + case PREFIX_OPERATOR: + case ARRAY_OPEN: + return true; + default: + return false; + } + } + + private boolean postfixOperatorAllowed() { + Token previousToken = getPreviousToken(); + + if (previousToken == null) { + return false; + } + + switch (previousToken.getType()) { + case BRACE_CLOSE: + case NUMBER_LITERAL: + case VARIABLE_OR_CONSTANT: + case STRING_LITERAL: + return true; + default: + return false; + } + } + + private boolean infixOperatorAllowed() { + Token previousToken = getPreviousToken(); + + if (previousToken == null) { + return false; + } + + switch (previousToken.getType()) { + case BRACE_CLOSE: + case VARIABLE_OR_CONSTANT: + case STRING_LITERAL: + case POSTFIX_OPERATOR: + case NUMBER_LITERAL: + case ARRAY_CLOSE: + return true; + default: + return false; + } + } + + private Token parseNumberLiteral() throws ParseException { + int nextChar = peekNextChar(); + if (currentChar == '0' && (nextChar == 'x' || nextChar == 'X')) { + return parseHexNumberLiteral(); + } else { + return parseDecimalNumberLiteral(); + } + } + + private Token parseDecimalNumberLiteral() throws ParseException { + int tokenStartIndex = currentColumnIndex; + StringBuilder tokenValue = new StringBuilder(); + + int lastChar = -1; + boolean scientificNotation = false; + boolean dotEncountered = false; + while (currentChar != -1 && isAtNumberChar()) { + if (currentChar == '.' && dotEncountered) { + tokenValue.append((char) currentChar); + throw new ParseException( + new Token(tokenStartIndex, tokenValue.toString(), TokenType.NUMBER_LITERAL), + "Number contains more than one decimal point"); + } + if (currentChar == '.') { + dotEncountered = true; + } + if (currentChar == 'e' || currentChar == 'E') { + scientificNotation = true; + } + tokenValue.append((char) currentChar); + lastChar = currentChar; + consumeChar(); + } + // illegal scientific format literal + if (scientificNotation + && (lastChar == 'e' + || lastChar == 'E' + || lastChar == '+' + || lastChar == '-' + || lastChar == '.')) { + throw new ParseException( + new Token(tokenStartIndex, tokenValue.toString(), TokenType.NUMBER_LITERAL), + "Illegal scientific format"); + } + return new Token(tokenStartIndex, tokenValue.toString(), TokenType.NUMBER_LITERAL); + } + + private Token parseHexNumberLiteral() { + int tokenStartIndex = currentColumnIndex; + StringBuilder tokenValue = new StringBuilder(); + + // hexadecimal number, consume "0x" + tokenValue.append((char) currentChar); + consumeChar(); + do { + tokenValue.append((char) currentChar); + consumeChar(); + } while (currentChar != -1 && isAtHexChar()); + return new Token(tokenStartIndex, tokenValue.toString(), TokenType.NUMBER_LITERAL); + } + + private Token parseIdentifier() throws ParseException { + int tokenStartIndex = currentColumnIndex; + StringBuilder tokenValue = new StringBuilder(); + while (currentChar != -1 && isAtIdentifierChar()) { + tokenValue.append((char) currentChar); + consumeChar(); + } + String tokenName = tokenValue.toString(); + + if (prefixOperatorAllowed() && operatorDictionary.hasPrefixOperator(tokenName)) { + return new Token( + tokenStartIndex, + tokenName, + TokenType.PREFIX_OPERATOR, + operatorDictionary.getPrefixOperator(tokenName)); + } else if (postfixOperatorAllowed() && operatorDictionary.hasPostfixOperator(tokenName)) { + return new Token( + tokenStartIndex, + tokenName, + TokenType.POSTFIX_OPERATOR, + operatorDictionary.getPostfixOperator(tokenName)); + } else if (operatorDictionary.hasInfixOperator(tokenName)) { + return new Token( + tokenStartIndex, + tokenName, + TokenType.INFIX_OPERATOR, + operatorDictionary.getInfixOperator(tokenName)); + } + + skipBlanks(); + if (currentChar == '(') { + if (!functionDictionary.hasFunction(tokenName)) { + throw new ParseException( + tokenStartIndex, + currentColumnIndex, + tokenName, + "Undefined function '" + tokenName + "'"); + } + FunctionIfc function = functionDictionary.getFunction(tokenName); + return new Token(tokenStartIndex, tokenName, TokenType.FUNCTION, function); + } else { + return new Token(tokenStartIndex, tokenName, TokenType.VARIABLE_OR_CONSTANT); + } + } + + Token parseStringLiteral() throws ParseException { + int startChar = currentChar; + int tokenStartIndex = currentColumnIndex; + StringBuilder tokenValue = new StringBuilder(); + // skip starting quote + consumeChar(); + boolean inQuote = true; + while (inQuote && currentChar != -1) { + if (currentChar == '\\') { + consumeChar(); + tokenValue.append(escapeCharacter(currentChar)); + } else if (currentChar == startChar) { + inQuote = false; + } else { + tokenValue.append((char) currentChar); + } + consumeChar(); + } + if (inQuote) { + throw new ParseException( + tokenStartIndex, currentColumnIndex, tokenValue.toString(), "Closing quote not found"); + } + return new Token(tokenStartIndex, tokenValue.toString(), TokenType.STRING_LITERAL); + } + + private char escapeCharacter(int character) throws ParseException { + switch (character) { + case '\'': + return '\''; + case '"': + return '"'; + case '\\': + return '\\'; + case 'n': + return '\n'; + case 'r': + return '\r'; + case 't': + return '\t'; + case 'b': + return '\b'; + case 'f': + return '\f'; + default: + throw new ParseException( + currentColumnIndex, 1, "\\" + (char) character, "Unknown escape character"); + } + } + + private boolean isAtNumberStart() { + if (Character.isDigit(currentChar)) { + return true; + } + return currentChar == '.' && Character.isDigit(peekNextChar()); + } + + private boolean isAtNumberChar() { + int previousChar = peekPreviousChar(); + + if ((previousChar == 'e' || previousChar == 'E') && currentChar != '.') { + return Character.isDigit(currentChar) || currentChar == '+' || currentChar == '-'; + } + + if (previousChar == '.' && currentChar != '.') { + return Character.isDigit(currentChar) || currentChar == 'e' || currentChar == 'E'; + } + + return Character.isDigit(currentChar) + || currentChar == '.' + || currentChar == 'e' + || currentChar == 'E'; + } + + private boolean isNextCharNumberChar() { + if (peekNextChar() == -1) { + return false; + } + consumeChar(); + boolean isAtNumber = isAtNumberChar(); + currentColumnIndex--; + currentChar = expressionString.charAt(currentColumnIndex - 1); + return isAtNumber; + } + + private boolean isAtHexChar() { + switch (currentChar) { + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + case 'a': + case 'b': + case 'c': + case 'd': + case 'e': + case 'f': + case 'A': + case 'B': + case 'C': + case 'D': + case 'E': + case 'F': + return true; + default: + return false; + } + } + + private boolean isAtIdentifierStart() { + return Character.isLetter(currentChar) || currentChar == '_'; + } + + private boolean isAtIdentifierChar() { + return Character.isLetter(currentChar) || Character.isDigit(currentChar) || currentChar == '_'; + } + + private boolean isAtStringLiteralStart() { + return currentChar == '"' + || currentChar == '\'' && configuration.isSingleQuoteStringLiteralsAllowed(); + } + + private void skipBlanks() { + if (currentChar == -2) { + // consume first character of expression + consumeChar(); + } + while (currentChar != -1 && Character.isWhitespace(currentChar)) { + consumeChar(); + } + } + + private int peekNextChar() { + return currentColumnIndex == expressionString.length() + ? -1 + : expressionString.charAt(currentColumnIndex); + } + + private int peekPreviousChar() { + return currentColumnIndex == 1 ? -1 : expressionString.charAt(currentColumnIndex - 2); + } + + private void consumeChar() { + if (currentColumnIndex == expressionString.length()) { + currentChar = -1; + } else { + currentChar = expressionString.charAt(currentColumnIndex++); + } + } +} From d2bb7ab0b2eb0971ee899bab1b576b548f5afabd Mon Sep 17 00:00:00 2001 From: brachy84 Date: Wed, 7 Jan 2026 12:07:35 +0100 Subject: [PATCH 47/50] fix missing setter in FluidDisplayWidget (cherry picked from commit d575828a48cd4ce334ec9d970092167351720262) --- .../modularui/widgets/FluidDisplayWidget.java | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/src/main/java/com/cleanroommc/modularui/widgets/FluidDisplayWidget.java b/src/main/java/com/cleanroommc/modularui/widgets/FluidDisplayWidget.java index 7ae6db1d4..95aa90d92 100644 --- a/src/main/java/com/cleanroommc/modularui/widgets/FluidDisplayWidget.java +++ b/src/main/java/com/cleanroommc/modularui/widgets/FluidDisplayWidget.java @@ -3,6 +3,7 @@ import com.cleanroommc.modularui.api.value.ISyncOrValue; import com.cleanroommc.modularui.api.value.IValue; import com.cleanroommc.modularui.screen.RichTooltip; +import com.cleanroommc.modularui.value.ObjectValue; import net.minecraftforge.fluids.FluidStack; @@ -14,6 +15,7 @@ public class FluidDisplayWidget extends AbstractFluidDisplayWidget { private IValue value; + private int capacity = 0; private boolean displayAmount = true; @Override @@ -37,11 +39,49 @@ protected boolean displayAmountText() { return this.value != null ? this.value.getValue() : null; } + @Override + public int getCapacity() { + return capacity; + } + + public FluidDisplayWidget value(IValue value) { + setSyncOrValue(value); + return this; + } + + public FluidDisplayWidget value(FluidStack value) { + return value(new ObjectValue<>(FluidStack.class, value)); + } + + /** + * Sets the capacity of the slot. This is only used for drawing and doesn't affect the actual capacity in any way. When the capacity is + * greater than zero, the fluid will be drawn partially depending on the fill level. + * + * @param capacity capacity for drawing the fluid + * @return this + */ + public FluidDisplayWidget capacity(int capacity) { + this.capacity = capacity; + return this; + } + + /** + * Sets whether the amount number should be displayed in the bottom left corner of the slot. + * + * @param displayAmount true if amount should be displayed + * @return this + */ public FluidDisplayWidget displayAmount(boolean displayAmount) { this.displayAmount = displayAmount; return this; } + /** + * Adds additional tooltip lines for the fluid. The function is called every frame. + * + * @param tooltip tooltip function for additional lines + * @return this + */ public FluidDisplayWidget fluidTooltip(BiConsumer tooltip) { return tooltipAutoUpdate(true).tooltipBuilder(t -> tooltip.accept(t, getFluidStack())); } From 8106b191c87994d1108f598e30c012bfff0e9ea3 Mon Sep 17 00:00:00 2001 From: brachy84 Date: Sat, 10 Jan 2026 17:02:51 +0100 Subject: [PATCH 48/50] fix UITexture.parseFromJson() (cherry picked from commit a6b7d0c3f2a950faead176f5e346433eb8020def) --- src/main/java/com/cleanroommc/modularui/drawable/UITexture.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/cleanroommc/modularui/drawable/UITexture.java b/src/main/java/com/cleanroommc/modularui/drawable/UITexture.java index c3b4be738..f31b96f38 100644 --- a/src/main/java/com/cleanroommc/modularui/drawable/UITexture.java +++ b/src/main/java/com/cleanroommc/modularui/drawable/UITexture.java @@ -227,7 +227,7 @@ public static UITexture parseFromJson(JsonObject json) { } UITexture uiTexture = builder.build(); uiTexture.colorOverride = JsonHelper.getColor(json, 0, "colorOverride"); - return builder.build(); + return uiTexture; } @Override From 217eb10d15732248652ee967f0b8ab0fc74ce17b Mon Sep 17 00:00:00 2001 From: brachy84 Date: Tue, 13 Jan 2026 15:38:04 +0100 Subject: [PATCH 49/50] fix --- .../modularui/network/ModularNetwork.java | 11 +- .../modularui/network/ModularNetworkSide.java | 6 +- .../network/packets/CloseAllGuiPacket.java | 8 +- .../network/packets/CloseGuiPacket.java | 12 +- .../network/packets/PacketSyncHandler.java | 7 +- .../network/packets/ReopenGuiPacket.java | 4 +- .../modularui/network/packets/SClipboard.java | 6 +- .../cleanroommc/modularui/test/TestTile.java | 506 +++++++++--------- .../modularui/widgets/SlotGroupWidget.java | 22 +- .../modularui/widgets/slot/ItemSlot.java | 2 +- 10 files changed, 289 insertions(+), 295 deletions(-) diff --git a/src/main/java/com/cleanroommc/modularui/network/ModularNetwork.java b/src/main/java/com/cleanroommc/modularui/network/ModularNetwork.java index 3c8444f1c..26b4023ec 100644 --- a/src/main/java/com/cleanroommc/modularui/network/ModularNetwork.java +++ b/src/main/java/com/cleanroommc/modularui/network/ModularNetwork.java @@ -8,9 +8,8 @@ import net.minecraft.client.gui.GuiScreen; import net.minecraft.entity.player.EntityPlayer; import net.minecraft.entity.player.EntityPlayerMP; -import net.minecraft.item.ItemStack; -import net.minecraftforge.fml.relauncher.Side; -import net.minecraftforge.fml.relauncher.SideOnly; +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; import org.jetbrains.annotations.ApiStatus; @@ -53,7 +52,7 @@ void sendPacket(IPacket packet, EntityPlayer player) { @Override void closeContainer(EntityPlayer player) { // mimics EntityPlayerSP.closeScreenAndDropStack() but without closing the screen - player.inventory.setItemStack(ItemStack.EMPTY); + player.inventory.setItemStack(null); player.openContainer = player.inventoryContainer; } @@ -64,14 +63,14 @@ public void closeContainer(int networkId, boolean dispose, EntityPlayerSP player @SideOnly(Side.CLIENT) public void closeAll() { - closeAll(Minecraft.getMinecraft().player); + closeAll(Minecraft.getMinecraft().thePlayer); } @SideOnly(Side.CLIENT) public void reopenSyncerOf(GuiScreen guiScreen) { if (guiScreen instanceof IMuiScreen ms && !ms.getScreen().isClientOnly()) { ModularSyncManager msm = ms.getScreen().getSyncManager(); - reopen(Minecraft.getMinecraft().player, msm, true); + reopen(Minecraft.getMinecraft().thePlayer, msm, true); } } } diff --git a/src/main/java/com/cleanroommc/modularui/network/ModularNetworkSide.java b/src/main/java/com/cleanroommc/modularui/network/ModularNetworkSide.java index 658dafbae..e6936db8f 100644 --- a/src/main/java/com/cleanroommc/modularui/network/ModularNetworkSide.java +++ b/src/main/java/com/cleanroommc/modularui/network/ModularNetworkSide.java @@ -11,8 +11,6 @@ import net.minecraft.entity.player.EntityPlayer; import net.minecraft.network.PacketBuffer; -import net.minecraftforge.common.MinecraftForge; -import net.minecraftforge.event.entity.player.PlayerContainerEvent; import it.unimi.dsi.fastutil.ints.Int2ReferenceOpenHashMap; import it.unimi.dsi.fastutil.objects.Reference2IntOpenHashMap; @@ -73,7 +71,7 @@ public void receivePacket(PacketSyncHandler packet) { ModularSyncManager msm = activeScreens.get(packet.networkId); if (msm == null) return; // silently discard packets for inactive screens try { - int id = packet.action ? 0 : packet.packet.readVarInt(); + int id = packet.action ? 0 : packet.packet.readVarIntFromBuffer(); msm.receiveWidgetUpdate(packet.panel, packet.key, packet.action, id, packet.packet); } catch (IndexOutOfBoundsException e) { ModularUI.LOGGER.error("Failed to read packet for sync handler {} in panel {}", packet.key, packet.panel); @@ -125,7 +123,7 @@ public void reopen(EntityPlayer player, ModularSyncManager msm, boolean sync) { closeContainer(player); player.openContainer = msm.getContainer(); msm.onOpen(); - MinecraftForge.EVENT_BUS.post(new PlayerContainerEvent.Open(player, msm.getContainer())); + // 1.12.2 fire event here which doesn't exist in 1.7.10 } if (sync) sendPacket(new ReopenGuiPacket(inverseActiveScreens.getInt(msm)), player); } diff --git a/src/main/java/com/cleanroommc/modularui/network/packets/CloseAllGuiPacket.java b/src/main/java/com/cleanroommc/modularui/network/packets/CloseAllGuiPacket.java index 5af158d2b..cd87d018e 100644 --- a/src/main/java/com/cleanroommc/modularui/network/packets/CloseAllGuiPacket.java +++ b/src/main/java/com/cleanroommc/modularui/network/packets/CloseAllGuiPacket.java @@ -3,12 +3,12 @@ import com.cleanroommc.modularui.network.IPacket; import com.cleanroommc.modularui.network.ModularNetwork; +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; import net.minecraft.client.Minecraft; import net.minecraft.client.network.NetHandlerPlayClient; import net.minecraft.network.NetHandlerPlayServer; import net.minecraft.network.PacketBuffer; -import net.minecraftforge.fml.relauncher.Side; -import net.minecraftforge.fml.relauncher.SideOnly; import org.jetbrains.annotations.Nullable; @@ -27,13 +27,13 @@ public void read(PacketBuffer buf) throws IOException {} @SideOnly(Side.CLIENT) @Override public @Nullable IPacket executeClient(NetHandlerPlayClient handler) { - ModularNetwork.CLIENT.closeAll(Minecraft.getMinecraft().player, false); + ModularNetwork.CLIENT.closeAll(Minecraft.getMinecraft().thePlayer, false); return null; } @Override public @Nullable IPacket executeServer(NetHandlerPlayServer handler) { - ModularNetwork.SERVER.closeAll(handler.player, false); + ModularNetwork.SERVER.closeAll(handler.playerEntity, false); return null; } } diff --git a/src/main/java/com/cleanroommc/modularui/network/packets/CloseGuiPacket.java b/src/main/java/com/cleanroommc/modularui/network/packets/CloseGuiPacket.java index 89084773e..c3107f2de 100644 --- a/src/main/java/com/cleanroommc/modularui/network/packets/CloseGuiPacket.java +++ b/src/main/java/com/cleanroommc/modularui/network/packets/CloseGuiPacket.java @@ -3,12 +3,12 @@ import com.cleanroommc.modularui.network.IPacket; import com.cleanroommc.modularui.network.ModularNetwork; +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; import net.minecraft.client.Minecraft; import net.minecraft.client.network.NetHandlerPlayClient; import net.minecraft.network.NetHandlerPlayServer; import net.minecraft.network.PacketBuffer; -import net.minecraftforge.fml.relauncher.Side; -import net.minecraftforge.fml.relauncher.SideOnly; import org.jetbrains.annotations.Nullable; @@ -28,26 +28,26 @@ public CloseGuiPacket(int networkId, boolean dispose) { @Override public void write(PacketBuffer buf) throws IOException { - buf.writeVarInt(this.networkId); + buf.writeVarIntToBuffer(this.networkId); buf.writeBoolean(this.dispose); } @Override public void read(PacketBuffer buf) throws IOException { - this.networkId = buf.readVarInt(); + this.networkId = buf.readVarIntFromBuffer(); this.dispose = buf.readBoolean(); } @SideOnly(Side.CLIENT) @Override public @Nullable IPacket executeClient(NetHandlerPlayClient handler) { - ModularNetwork.CLIENT.closeContainer(this.networkId, this.dispose, Minecraft.getMinecraft().player, false); + ModularNetwork.CLIENT.closeContainer(this.networkId, this.dispose, Minecraft.getMinecraft().thePlayer, false); return null; } @Override public @Nullable IPacket executeServer(NetHandlerPlayServer handler) { - ModularNetwork.SERVER.closeContainer(this.networkId, this.dispose, handler.player, false); + ModularNetwork.SERVER.closeContainer(this.networkId, this.dispose, handler.playerEntity, false); return null; } } diff --git a/src/main/java/com/cleanroommc/modularui/network/packets/PacketSyncHandler.java b/src/main/java/com/cleanroommc/modularui/network/packets/PacketSyncHandler.java index 161f5cb99..915376a03 100644 --- a/src/main/java/com/cleanroommc/modularui/network/packets/PacketSyncHandler.java +++ b/src/main/java/com/cleanroommc/modularui/network/packets/PacketSyncHandler.java @@ -3,15 +3,12 @@ import com.cleanroommc.modularui.network.IPacket; import com.cleanroommc.modularui.network.ModularNetwork; import com.cleanroommc.modularui.network.NetworkUtils; -import com.cleanroommc.modularui.screen.ModularContainer; -import com.cleanroommc.modularui.screen.ModularScreen; -import com.cleanroommc.modularui.value.sync.ModularSyncManager; import net.minecraft.client.network.NetHandlerPlayClient; import net.minecraft.network.NetHandlerPlayServer; import net.minecraft.network.PacketBuffer; -import net.minecraftforge.fml.relauncher.Side; -import net.minecraftforge.fml.relauncher.SideOnly; +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.Nullable; diff --git a/src/main/java/com/cleanroommc/modularui/network/packets/ReopenGuiPacket.java b/src/main/java/com/cleanroommc/modularui/network/packets/ReopenGuiPacket.java index 905854f2d..2933456e7 100644 --- a/src/main/java/com/cleanroommc/modularui/network/packets/ReopenGuiPacket.java +++ b/src/main/java/com/cleanroommc/modularui/network/packets/ReopenGuiPacket.java @@ -34,13 +34,13 @@ public void read(PacketBuffer buf) throws IOException { @Override public @Nullable IPacket executeClient(NetHandlerPlayClient handler) { - ModularNetwork.CLIENT.reopen(Minecraft.getMinecraft().player, this.networkId, false); + ModularNetwork.CLIENT.reopen(Minecraft.getMinecraft().thePlayer, this.networkId, false); return null; } @Override public @Nullable IPacket executeServer(NetHandlerPlayServer handler) { - ModularNetwork.SERVER.reopen(handler.player, this.networkId, false); + ModularNetwork.SERVER.reopen(handler.playerEntity, this.networkId, false); return null; } } diff --git a/src/main/java/com/cleanroommc/modularui/network/packets/SClipboard.java b/src/main/java/com/cleanroommc/modularui/network/packets/SClipboard.java index 5fc72ba64..56a1df711 100644 --- a/src/main/java/com/cleanroommc/modularui/network/packets/SClipboard.java +++ b/src/main/java/com/cleanroommc/modularui/network/packets/SClipboard.java @@ -9,9 +9,9 @@ import net.minecraft.entity.player.EntityPlayer; import net.minecraft.entity.player.EntityPlayerMP; import net.minecraft.network.PacketBuffer; -import net.minecraftforge.fml.common.FMLCommonHandler; -import net.minecraftforge.fml.relauncher.Side; -import net.minecraftforge.fml.relauncher.SideOnly; +import cpw.mods.fml.common.FMLCommonHandler; +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; public class SClipboard implements IPacket { diff --git a/src/main/java/com/cleanroommc/modularui/test/TestTile.java b/src/main/java/com/cleanroommc/modularui/test/TestTile.java index 4fdc3c04b..f1b37c982 100644 --- a/src/main/java/com/cleanroommc/modularui/test/TestTile.java +++ b/src/main/java/com/cleanroommc/modularui/test/TestTile.java @@ -29,7 +29,6 @@ import com.cleanroommc.modularui.value.BoolValue; import com.cleanroommc.modularui.value.IntValue; import com.cleanroommc.modularui.value.StringValue; -import com.cleanroommc.modularui.value.sync.DoubleSyncValue; import com.cleanroommc.modularui.value.sync.DynamicSyncHandler; import com.cleanroommc.modularui.value.sync.GenericSyncValue; import com.cleanroommc.modularui.value.sync.IntSyncValue; @@ -188,18 +187,18 @@ public ModularPanel buildUI(PosGuiData guiData, PanelSyncManager syncManager, UI .align(Alignment.Center); // center the panel in the screen panel .child(new Row() - .name("Tab row") - .coverChildren() - .topRel(0f, 4, 1f) - .child(new PageButton(0, tabController) - .tab(GuiTextures.TAB_TOP, -1)) - .child(new PageButton(1, tabController) - .tab(GuiTextures.TAB_TOP, 0)) - .child(new PageButton(2, tabController) - .tab(GuiTextures.TAB_TOP, 0)) - .child(new PageButton(3, tabController) - .tab(GuiTextures.TAB_TOP, 0) - .overlay(new ItemDrawable(Blocks.chest).asIcon())) + .name("Tab row") + .coverChildren() + .topRel(0f, 4, 1f) + .child(new PageButton(0, tabController) + .tab(GuiTextures.TAB_TOP, -1)) + .child(new PageButton(1, tabController) + .tab(GuiTextures.TAB_TOP, 0)) + .child(new PageButton(2, tabController) + .tab(GuiTextures.TAB_TOP, 0)) + .child(new PageButton(3, tabController) + .tab(GuiTextures.TAB_TOP, 0) + .overlay(new ItemDrawable(Blocks.chest).asIcon())) /*.child(new PageButton(4, tabController) // schema renderer not working .tab(GuiTextures.TAB_TOP, 0) .overlay(new ItemDrawable(Items.ender_eye).asIcon()))*/) @@ -237,256 +236,257 @@ public ModularPanel buildUI(PosGuiData guiData, PanelSyncManager syncManager, UI .expanded() .widthRel(1f) .child(new PagedWidget<>() - .name("root parent") - .sizeRel(1f) - .controller(tabController) - .addPage(new ParentWidget<>() - .name("page 1 parent") - .sizeRel(1f, 1f) - .padding(7, 0) - .child(new Row() - .name("buttons, slots and more tests") - .height(137) - .coverChildrenWidth() - .verticalCenter() - //.padding(7) - .child(new Column() - .name("buttons and slots test") + .name("root parent") + .sizeRel(1f) + .controller(tabController) + .addPage(new ParentWidget<>() + .name("page 1 parent") + .sizeRel(1f, 1f) + .padding(7, 0) + .child(new Row() + .name("buttons, slots and more tests") + .height(137) + .coverChildrenWidth() + .verticalCenter() + //.padding(7) + .child(new Column() + .name("buttons and slots test") + .coverChildren() + .marginRight(8) + //.flex(flex -> flex.height(0.5f)) + //.widthRel(0.5f) + .crossAxisAlignment(Alignment.CrossAxis.CENTER) + .child(new ButtonWidget<>() + .size(60, 18) + .overlay(IKey.dynamic(() -> "Button " + this.val))) + .child(new FluidSlot() + .margin(2) + .syncHandler(SyncHandlers.fluidSlot(this.fluidTank))) + .child(new ButtonWidget<>() + .size(60, 18) + .tooltip(tooltip -> { + tooltip.showUpTimer(10); + tooltip.addLine(IKey.str("Test Line g")); + tooltip.addLine(IKey.str("An image inside of a tooltip:")); + tooltip.addLine(GuiTextures.MUI_LOGO.asIcon().size(50).alignment(Alignment.TopCenter)); + tooltip.addLine(IKey.str("And here a circle:")); + tooltip.addLine(new Circle() + .setColor(Color.RED.darker(2), Color.RED.brighter(2)) + .asIcon() + .size(20)) + .addLine(new ItemDrawable(new ItemStack(Items.diamond)).asIcon()) + .pos(RichTooltip.Pos.LEFT); + }) + .onMousePressed(mouseButton -> { + //panel.getScreen().close(true); + //panel.getScreen().openDialog("dialog", this::buildDialog, ModularUI.LOGGER::info); + //openSecondWindow(context).openIn(panel.getScreen()); + panelSyncHandler.openPanel(); + return true; + }) + //.flex(flex -> flex.left(3)) // ? + .overlay(IKey.str("Button 2"))) + .child(new TextFieldWidget() + .addTooltipLine("this tooltip is overridden") + .size(60, 18) + .setTextAlignment(Alignment.Center) + .value(SyncHandlers.string(() -> this.value, val -> this.value = val)) + .margin(0, 2) + .hintText("hint")) + .child(new TextFieldWidget() + .size(60, 18) + .paddingTop(1) + .syncHandler("textFieldSyncer") + .setScrollValues(10, 0.1, 100) + .setNumbersDouble(Function.identity()) + .hintText("number")) + //.child(IKey.str("Test string").asWidget().padding(2).debugName("test string")) + .child(new ScrollingTextWidget(IKey.str("Very very long test string")).widthRel(1f).height(16)) + //.child(IKey.EMPTY.asWidget().debugName("Empty IKey")) + ) + .child(new Column() + .name("button and slots test 2") .coverChildren() - .marginRight(8) - //.flex(flex -> flex.height(0.5f)) //.widthRel(0.5f) .crossAxisAlignment(Alignment.CrossAxis.CENTER) - .child(new ButtonWidget<>() - .size(60, 18) - .overlay(IKey.dynamic(() -> "Button " + this.val))) + .child(new ProgressWidget() + .progress(() -> this.progress / (double) this.duration) + .texture(GuiTextures.PROGRESS_ARROW, 20)) + .child(new ProgressWidget() + .progress(() -> this.progress / (double) this.duration) + .texture(GuiTextures.PROGRESS_CYCLE, 20) + .direction(ProgressWidget.Direction.CIRCULAR_CW)) + .child(new Row().coverChildrenWidth().height(18) + .reverseLayout(false) + .child(new ToggleButton() + .value(new BoolValue.Dynamic(() -> cycleStateValue.getIntValue() == 0, val -> cycleStateValue.setIntValue(0))) + .overlay(GuiTextures.CYCLE_BUTTON_DEMO.getSubArea(0, 0, 1, 1 / 3f))) + .child(new ToggleButton() + .value(new BoolValue.Dynamic(() -> cycleStateValue.getIntValue() == 1, val -> cycleStateValue.setIntValue(1))) + .overlay(GuiTextures.CYCLE_BUTTON_DEMO.getSubArea(0, 1 / 3f, 1, 2 / 3f))) + .child(new ToggleButton() + .value(new BoolValue.Dynamic(() -> cycleStateValue.getIntValue() == 2, val -> cycleStateValue.setIntValue(2))) + .overlay(GuiTextures.CYCLE_BUTTON_DEMO.getSubArea(0, 2 / 3f, 1, 1)))) + /*.child(new CycleButtonWidget() + .length(3) + .texture(GuiTextures.CYCLE_BUTTON_DEMO) + .addTooltip(0, "State 1") + .addTooltip(1, "State 2") + .addTooltip(2, "State 3") + .background(GuiTextures.BUTTON) + .value(SyncHandlers.intNumber(() -> this.cycleState, val -> this.cycleState = val)))*/ + .child(new ItemSlot() + .slot(SyncHandlers.itemSlot(this.inventory, 0).ignoreMaxStackSize(true).singletonSlotGroup())) .child(new FluidSlot() .margin(2) - .syncHandler(SyncHandlers.fluidSlot(this.fluidTank))) - .child(new ButtonWidget<>() - .size(60, 18) - .tooltip(tooltip -> { - tooltip.showUpTimer(10); - tooltip.addLine(IKey.str("Test Line g")); - tooltip.addLine(IKey.str("An image inside of a tooltip:")); - tooltip.addLine(GuiTextures.MUI_LOGO.asIcon().size(50).alignment(Alignment.TopCenter)); - tooltip.addLine(IKey.str("And here a circle:")); - tooltip.addLine(new Circle() - .setColor(Color.RED.darker(2), Color.RED.brighter(2)) - .asIcon() - .size(20)) - .addLine(new ItemDrawable(new ItemStack(Items.diamond)).asIcon()) - .pos(RichTooltip.Pos.LEFT); - }) - .onMousePressed(mouseButton -> { - //panel.getScreen().close(true); - //panel.getScreen().openDialog("dialog", this::buildDialog, ModularUI.LOGGER::info); - //openSecondWindow(context).openIn(panel.getScreen()); - panelSyncHandler.openPanel(); - return true; - }) - //.flex(flex -> flex.left(3)) // ? - .overlay(IKey.str("Button 2"))) - .child(new TextFieldWidget() - .addTooltipLine("this tooltip is overridden") - .size(60, 18) - .setTextAlignment(Alignment.Center) - .value(SyncHandlers.string(() -> this.value, val -> this.value = val)) - .margin(0, 2) - .hintText("hint")) - .child(new TextFieldWidget() - .size(60, 18) - .paddingTop(1) - .syncHandler("textFieldSyncer") - .setScrollValues(10, 0.1, 100) - .setNumbersDouble(Function.identity()) - .hintText("number")) - //.child(IKey.str("Test string").asWidget().padding(2).debugName("test string")) - .child(new ScrollingTextWidget(IKey.str("Very very long test string")).widthRel(1f).height(16)) - //.child(IKey.EMPTY.asWidget().debugName("Empty IKey")) - ) - .child(new Column() - .name("button and slots test 2") + .width(30) + .alwaysShowFull(false) + .syncHandler(SyncHandlers.fluidSlot(this.fluidTankPhantom).phantom(true))) + .child(new Column() + .name("button and slots test 3") + .coverChildren() + .child(new TextFieldWidget() + .size(60, 20) + .value(SyncHandlers.intNumber(() -> this.intValue, val -> this.intValue = val)) + .setScrollValues(1, 0.5, 2.5) + .setNumbers(0, 9999999) + .setFormatAsInteger(true) + .hintText("integer"))) + ))) + .addPage(new Column() + .name("Slots test page") .coverChildren() - //.widthRel(0.5f) - .crossAxisAlignment(Alignment.CrossAxis.CENTER) - .child(new ProgressWidget() - .progress(() -> this.progress / (double) this.duration) - .texture(GuiTextures.PROGRESS_ARROW, 20)) - .child(new ProgressWidget() - .progress(() -> this.progress / (double) this.duration) - .texture(GuiTextures.PROGRESS_CYCLE, 20) - .direction(ProgressWidget.Direction.CIRCULAR_CW)) - .child(new Row().coverChildrenWidth().height(18) - .reverseLayout(false) - .child(new ToggleButton() - .value(new BoolValue.Dynamic(() -> cycleStateValue.getIntValue() == 0, val -> cycleStateValue.setIntValue(0))) - .overlay(GuiTextures.CYCLE_BUTTON_DEMO.getSubArea(0, 0, 1, 1 / 3f))) - .child(new ToggleButton() - .value(new BoolValue.Dynamic(() -> cycleStateValue.getIntValue() == 1, val -> cycleStateValue.setIntValue(1))) - .overlay(GuiTextures.CYCLE_BUTTON_DEMO.getSubArea(0, 1 / 3f, 1, 2 / 3f))) - .child(new ToggleButton() - .value(new BoolValue.Dynamic(() -> cycleStateValue.getIntValue() == 2, val -> cycleStateValue.setIntValue(2))) - .overlay(GuiTextures.CYCLE_BUTTON_DEMO.getSubArea(0, 2 / 3f, 1, 1)))) - /*.child(new CycleButtonWidget() - .length(3) - .texture(GuiTextures.CYCLE_BUTTON_DEMO) - .addTooltip(0, "State 1") - .addTooltip(1, "State 2") - .addTooltip(2, "State 3") - .background(GuiTextures.BUTTON) - .value(SyncHandlers.intNumber(() -> this.cycleState, val -> this.cycleState = val)))*/ - .child(new ItemSlot() - .slot(SyncHandlers.itemSlot(this.inventory, 0).ignoreMaxStackSize(true).singletonSlotGroup())) - .child(new FluidSlot() - .margin(2) - .width(30) - .alwaysShowFull(false) - .syncHandler(SyncHandlers.fluidSlot(this.fluidTankPhantom).phantom(true))) - .child(new Column() - .name("button and slots test 3") - .coverChildren() - .child(new TextFieldWidget() - .size(60, 20) - .value(SyncHandlers.intNumber(() -> this.intValue, val -> this.intValue = val)) - .setScrollValues(1, 0.5, 2.5) - .setNumbers(0, 9999999) - .setFormatAsInteger(true) - .hintText("integer"))) - ))) - .addPage(new Column() - .name("Slots test page") - .coverChildren() - //.height(120) - .padding(7) - .alignX(0.5f) - .mainAxisAlignment(Alignment.MainAxis.START) - .childPadding(2) - //.child(SlotGroupWidget.playerInventory().left(0)) - .child(SlotGroupWidget.builder() - .matrix("III", "III", "III") - .key('I', index -> { - // 4 is the middle slot with a negative priority -> shift click prioritises middle slot - if (index == 4) { - return new ItemSlot().slot(SyncHandlers.itemSlot(this.bigInventory, index).singletonSlotGroup(-100)); - } - return new ItemSlot().slot(SyncHandlers.itemSlot(this.bigInventory, index).slotGroup("item_inv")); - }) - .build().name("9 slot inv") - .placeSortButtonsTopRightVertical() - //.marginBottom(2) - ) - .child(SlotGroupWidget.builder() - .row("FII") - .row("FII") - .key('F', index -> new FluidSlot().syncHandler("mixer_fluids", index)) - .key('I', index -> ItemSlot.create(index >= 2).slot(new ModularSlot(this.mixerItems, index).slotGroup("mixer_items"))) - .build().name("mixer inv") - .disableSortButtons()) - .child(new Row() - .coverChildrenHeight() - .child(new CycleButtonWidget() - .size(14, 14) - .stateCount(3) - .stateOverlay(GuiTextures.CYCLE_BUTTON_DEMO) - .value(new IntSyncValue(() -> this.val2, val -> this.val2 = val)) - .margin(8, 0)) - .child(IKey.str("Hello World").asWidget().height(18))) - .child(new SpecialButton(IKey.str("A very long string that looks cool when animated").withAnimation()) - .height(14) - .widthRel(1f)) + //.height(120) + .padding(7) + .alignX(0.5f) + .mainAxisAlignment(Alignment.MainAxis.START) + .childPadding(2) + //.child(SlotGroupWidget.playerInventory().left(0)) + .child(SlotGroupWidget.builder() + .matrix("III", "III", "III") + .key('I', index -> { + // 4 is the middle slot with a negative priority -> shift click prioritises middle slot + if (index == 4) { + return new ItemSlot().slot(SyncHandlers.itemSlot(this.bigInventory, index).singletonSlotGroup(-100)); + } + return new ItemSlot().slot(SyncHandlers.itemSlot(this.bigInventory, index).slotGroup("item_inv")); + }) + .build().name("9 slot inv") + //.placeSortButtonsTopRightVertical() + //.marginBottom(2) + ) + .child(SlotGroupWidget.builder() + .row("FII") + .row("FII") + .key('F', index -> new FluidSlot().syncHandler("mixer_fluids", index)) + .key('I', index -> ItemSlot.create(index >= 2).slot(new ModularSlot(this.mixerItems, index).slotGroup("mixer_items"))) + .build().name("mixer inv") + //.disableSortButtons() + ) + .child(new Row() + .coverChildrenHeight() + .child(new CycleButtonWidget() + .size(14, 14) + .stateCount(3) + .stateOverlay(GuiTextures.CYCLE_BUTTON_DEMO) + .value(new IntSyncValue(() -> this.val2, val -> this.val2 = val)) + .margin(8, 0)) + .child(IKey.str("Hello World").asWidget().height(18))) + .child(new SpecialButton(IKey.str("A very long string that looks cool when animated").withAnimation()) + .height(14) + .widthRel(1f)) /*GuiTextures.LOGO.asIcon() .size(80, 80) .asWidget() .flex(flex -> flex.width(1f).height(1f))*/) - .addPage(new ParentWidget<>() - .name("page 3 parent") - .sizeRel(1f, 1f) - .padding(7) - //.child(SlotGroupWidget.playerInventory()) - .child(new SliderWidget() - .widthRel(1f).bottom(50).height(16) // test overwriting of units - .top(7) - .stopper(0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100) - .background(GuiTextures.SLOT_FLUID)) - .child(new ButtonWidget<>() - .name("color picker button") - .top(25) - .background(colorPickerBackground) - .disableHoverBackground() - .onMousePressed(mouseButton -> { - colorPicker.openPanel(); - return true; - })) - .child(new ListWidget<>() - .name("test config list") - .widthRel(1f).top(50).bottom(2) - /*.child(new Rectangle().setColor(0xFF606060).asWidget() - .top(1) - .left(32) - .size(1, 40))*/ - .child(new Row() - .name("test config 1") - .widthRel(1f).coverChildrenHeight() - .crossAxisAlignment(Alignment.CrossAxis.CENTER) - .childPadding(2) - .child(new CycleButtonWidget() - .value(new BoolValue(false)) - .stateOverlay(GuiTextures.CHECK_BOX) - .size(14, 14) - .margin(8, 4)) - .child(IKey.str("Boolean config").asWidget() - .height(14))) - .child(new Row() - .name("test config 2") - .widthRel(1f).height(14) - .childPadding(2) - .child(new TextFieldWidget() - .value(new IntValue.Dynamic(() -> this.num, val -> this.num = val)) - .disableHoverBackground() - .setNumbers(1, Short.MAX_VALUE) - .setTextAlignment(Alignment.Center) - .background(new Rectangle().color(0xFFb1b1b1)) - .setTextColor(IKey.TEXT_COLOR) - .size(20, 14)) - .child(IKey.str("Number config").asWidget() - .height(14))) - .child(IKey.str("Config title").asWidget() - .color(0xFF404040) - .alignment(Alignment.CenterLeft) - .left(5).height(14) - .tooltip(tooltip -> tooltip.showUpTimer(10) - .addLine(IKey.str("Config title tooltip")))) - .child(new Row() - .name("test config 3") - .widthRel(1f).height(14) - .childPadding(2) - .child(new CycleButtonWidget() - .value(new BoolValue(false)) - .stateOverlay(GuiTextures.CHECK_BOX) - .size(14, 14)) - .child(IKey.str("Boolean config 3").asWidget() - .height(14))))) - .addPage(new ParentWidget<>() - .name("page 4 storage") - .sizeRel(1f) - .child(new Column() - .child(new EntityDisplayWidget(()->fool) - .doesLookAtMouse(true) - .asWidget() - .tooltip(t-> t.addLine("Please don't bully me"))) + .addPage(new ParentWidget<>() + .name("page 3 parent") + .sizeRel(1f, 1f) .padding(7) - .child(new ItemSlot() - .slot(new ModularSlot(this.storageInventory0, 0) - .changeListener(((newItem, onlyAmountChanged, client, init) -> { - if (client && !onlyAmountChanged) { - dynamicSyncHandler.notifyUpdate(packet -> NetworkUtils.writeItemStack(packet, newItem)); - } - })))) - .child(new DynamicSyncedWidget<>() - .widthRel(1f) - .syncHandler(dynamicSyncHandler))) - ) + //.child(SlotGroupWidget.playerInventory()) + .child(new SliderWidget() + .widthRel(1f).bottom(50).height(16) // test overwriting of units + .top(7) + .stopper(0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100) + .background(GuiTextures.SLOT_FLUID)) + .child(new ButtonWidget<>() + .name("color picker button") + .top(25) + .background(colorPickerBackground) + .disableHoverBackground() + .onMousePressed(mouseButton -> { + colorPicker.openPanel(); + return true; + })) + .child(new ListWidget<>() + .name("test config list") + .widthRel(1f).top(50).bottom(2) + /*.child(new Rectangle().setColor(0xFF606060).asWidget() + .top(1) + .left(32) + .size(1, 40))*/ + .child(new Row() + .name("test config 1") + .widthRel(1f).coverChildrenHeight() + .crossAxisAlignment(Alignment.CrossAxis.CENTER) + .childPadding(2) + .child(new CycleButtonWidget() + .value(new BoolValue(false)) + .stateOverlay(GuiTextures.CHECK_BOX) + .size(14, 14) + .margin(8, 4)) + .child(IKey.str("Boolean config").asWidget() + .height(14))) + .child(new Row() + .name("test config 2") + .widthRel(1f).height(14) + .childPadding(2) + .child(new TextFieldWidget() + .value(new IntValue.Dynamic(() -> this.num, val -> this.num = val)) + .disableHoverBackground() + .setNumbers(1, Short.MAX_VALUE) + .setTextAlignment(Alignment.Center) + .background(new Rectangle().color(0xFFb1b1b1)) + .setTextColor(IKey.TEXT_COLOR) + .size(20, 14)) + .child(IKey.str("Number config").asWidget() + .height(14))) + .child(IKey.str("Config title").asWidget() + .color(0xFF404040) + .alignment(Alignment.CenterLeft) + .left(5).height(14) + .tooltip(tooltip -> tooltip.showUpTimer(10) + .addLine(IKey.str("Config title tooltip")))) + .child(new Row() + .name("test config 3") + .widthRel(1f).height(14) + .childPadding(2) + .child(new CycleButtonWidget() + .value(new BoolValue(false)) + .stateOverlay(GuiTextures.CHECK_BOX) + .size(14, 14)) + .child(IKey.str("Boolean config 3").asWidget() + .height(14))))) + .addPage(new ParentWidget<>() + .name("page 4 storage") + .sizeRel(1f) + .child(new Column() + .child(new EntityDisplayWidget(() -> fool) + .doesLookAtMouse(true) + .asWidget() + .tooltip(t -> t.addLine("Please don't bully me"))) + .padding(7) + .child(new ItemSlot() + .slot(new ModularSlot(this.storageInventory0, 0) + .changeListener(((newItem, onlyAmountChanged, client, init) -> { + if (client && !onlyAmountChanged) { + dynamicSyncHandler.notifyUpdate(packet -> NetworkUtils.writeItemStack(packet, newItem)); + } + })))) + .child(new DynamicSyncedWidget<>() + .widthRel(1f) + .syncHandler(dynamicSyncHandler))) + ) //.addPage(createSchemaPage(guiData)) // schema renderer not working )) .child(SlotGroupWidget.playerInventory(false)) @@ -511,7 +511,7 @@ private IWidget createSchemaPage(GuiData data) { page.child(IKey.str("schema").asWidget()); if (worldObj.isRemote) page.child(new SchemaWidget(new SchemaRenderer(ArraySchema.builder().layer("a").where('a', Blocks.diamond_ore).build()) - .highlightRenderer(new BlockHighlight(Color.withAlpha(Color.GREEN.brighter(1), 0.9f), 1/32f))) + .highlightRenderer(new BlockHighlight(Color.withAlpha(Color.GREEN.brighter(1), 0.9f), 1 / 32f))) .pos(20, 20) .size(100, 100)); return page; diff --git a/src/main/java/com/cleanroommc/modularui/widgets/SlotGroupWidget.java b/src/main/java/com/cleanroommc/modularui/widgets/SlotGroupWidget.java index 6ec403d4e..7e4bc8d5f 100644 --- a/src/main/java/com/cleanroommc/modularui/widgets/SlotGroupWidget.java +++ b/src/main/java/com/cleanroommc/modularui/widgets/SlotGroupWidget.java @@ -14,7 +14,6 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; -import java.util.function.Consumer; import java.util.function.IntFunction; public class SlotGroupWidget extends ParentWidget { @@ -61,20 +60,21 @@ public static SlotGroupWidget playerInventory(SlotConsumer slotConsumer) { private String slotGroupName; private SlotGroup slotGroup; - private boolean sortButtonsAdded = false; - private Consumer sortButtonsEditor; + //private boolean sortButtonsAdded = false; + //private Consumer sortButtonsEditor; @Override public void onInit() { super.onInit(); - if (!this.sortButtonsAdded) { + // TODO: bogo compat + /*if (!this.sortButtonsAdded) { SortButtons sb = new SortButtons(); if (this.sortButtonsEditor == null) placeSortButtonsTopRightHorizontal(); if (getName() != null) { sb.name(getName() + "_sorter_buttons"); } child(sb); - } + }*/ } @Override @@ -98,7 +98,7 @@ public void afterInit() { @Override protected void onChildAdd(IWidget child) { super.onChildAdd(child); - if (child instanceof SortButtons sortButtons) { + /*if (child instanceof SortButtons sortButtons) { this.sortButtonsAdded = true; if (sortButtons.getSlotGroup() == null && sortButtons.getSlotGroupName() == null) { if (this.slotGroup != null) { @@ -110,13 +110,13 @@ protected void onChildAdd(IWidget child) { if (this.sortButtonsEditor != null) { this.sortButtonsEditor.accept(sortButtons); } - } + }*/ } - public SlotGroupWidget disableSortButtons() { + /*public SlotGroupWidget disableSortButtons() { this.sortButtonsAdded = true; return this; - } + }*/ public void setSlotsSynced(String name) { int i = 0; @@ -128,7 +128,7 @@ public void setSlotsSynced(String name) { } } - public SlotGroupWidget editSortButtons(Consumer sortButtonsEditor) { + /*public SlotGroupWidget editSortButtons(Consumer sortButtonsEditor) { this.sortButtonsEditor = sortButtonsEditor; return this; } @@ -153,7 +153,7 @@ public SlotGroupWidget placeSortButtonsTopRightHorizontal(Consumer sb.horizontal().bottomRelOffset(1f, 1).right(0); if (additionalEdits != null) additionalEdits.accept(sb); }); - } + }*/ public SlotGroupWidget slotGroup(String slotGroupName) { this.slotGroupName = slotGroupName; diff --git a/src/main/java/com/cleanroommc/modularui/widgets/slot/ItemSlot.java b/src/main/java/com/cleanroommc/modularui/widgets/slot/ItemSlot.java index feeb1c46e..5e1714730 100644 --- a/src/main/java/com/cleanroommc/modularui/widgets/slot/ItemSlot.java +++ b/src/main/java/com/cleanroommc/modularui/widgets/slot/ItemSlot.java @@ -223,7 +223,7 @@ private void drawSlot(ModularSlot slotIn) { RenderItem renderItem = GuiScreenAccessor.getItemRender(); ItemStack itemstack = slotIn.getStack(); boolean isDragPreview = false; - boolean flag1 = slotIn == acc.getClickedSlot() && acc.getDraggedStack() != null && !acc.getIsRightMouseClick(); + boolean doDrawItem = slotIn == acc.getClickedSlot() && acc.getDraggedStack() != null && !acc.getIsRightMouseClick(); ItemStack itemstack1 = guiScreen.mc.thePlayer.inventory.getItemStack(); int amount = -1; String format = null; From f768e4a31acbaa6e3a8a71352f458398134fdcf0 Mon Sep 17 00:00:00 2001 From: brachy84 Date: Tue, 13 Jan 2026 15:55:54 +0100 Subject: [PATCH 50/50] method to remove current tooltips (cherry picked from commit 524838ad5017d51059b290d1d348c418f7c6d78e) --- .../modularui/api/drawable/IRichTextBuilder.java | 11 ++++++++++- .../cleanroommc/modularui/api/widget/ITooltip.java | 5 +++++ .../modularui/drawable/text/RichText.java | 14 ++++++++++++++ .../cleanroommc/modularui/screen/RichTooltip.java | 8 +++++--- 4 files changed, 34 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/cleanroommc/modularui/api/drawable/IRichTextBuilder.java b/src/main/java/com/cleanroommc/modularui/api/drawable/IRichTextBuilder.java index a3197c41a..1dc19aa52 100644 --- a/src/main/java/com/cleanroommc/modularui/api/drawable/IRichTextBuilder.java +++ b/src/main/java/com/cleanroommc/modularui/api/drawable/IRichTextBuilder.java @@ -1,6 +1,5 @@ package com.cleanroommc.modularui.api.drawable; -import com.cleanroommc.modularui.drawable.text.RichText; import com.cleanroommc.modularui.drawable.text.Spacer; import com.cleanroommc.modularui.utils.Alignment; @@ -13,6 +12,16 @@ public interface IRichTextBuilder> { IRichTextBuilder getRichText(); + /** + * Removes all text and style. + * + * @return this + */ + default T reset() { + getRichText().reset(); + return getThis(); + } + /** * Adds a string to the current line * diff --git a/src/main/java/com/cleanroommc/modularui/api/widget/ITooltip.java b/src/main/java/com/cleanroommc/modularui/api/widget/ITooltip.java index fbaff656a..4fc5102c0 100644 --- a/src/main/java/com/cleanroommc/modularui/api/widget/ITooltip.java +++ b/src/main/java/com/cleanroommc/modularui/api/widget/ITooltip.java @@ -271,4 +271,9 @@ default W addTooltipStringLines(Iterable lines) { tooltip().addStringLines(lines); return getThis(); } + + default W removeAllTooltips() { + tooltip().reset(); + return getThis(); + } } diff --git a/src/main/java/com/cleanroommc/modularui/drawable/text/RichText.java b/src/main/java/com/cleanroommc/modularui/drawable/text/RichText.java index 9606a2d4c..4c16dfdcf 100644 --- a/src/main/java/com/cleanroommc/modularui/drawable/text/RichText.java +++ b/src/main/java/com/cleanroommc/modularui/drawable/text/RichText.java @@ -93,6 +93,20 @@ public IRichTextBuilder getRichText() { return this; } + @Override + public RichText reset() { + this.elements.clear(); + this.stringList = null; + this.alignment = Alignment.CenterLeft; + this.scale = 1f; + this.color = null; + this.shadow = null; + this.cursor = 0; + this.cursorLocked = false; + this.cachedText = null; + return this; + } + public RichText add(String s) { addElement(s); clearStrings(); diff --git a/src/main/java/com/cleanroommc/modularui/screen/RichTooltip.java b/src/main/java/com/cleanroommc/modularui/screen/RichTooltip.java index 6417cbea5..76bc85a45 100644 --- a/src/main/java/com/cleanroommc/modularui/screen/RichTooltip.java +++ b/src/main/java/com/cleanroommc/modularui/screen/RichTooltip.java @@ -54,8 +54,9 @@ public RichTooltip() { parent(Area.ZERO); } - public void reset() { - clearText(); + @Override + public RichTooltip reset() { + this.text.reset(); this.pos = null; this.tooltipBuilder = null; this.showUpTimer = 0; @@ -65,6 +66,7 @@ public void reset() { this.x = 0; this.y = 0; this.maxWidth = Integer.MAX_VALUE; + return this; } public RichTooltip parent(Consumer parent) { @@ -335,7 +337,7 @@ public RichTooltip showUpTimer(int showUpTimer) { public RichTooltip tooltipBuilder(Consumer tooltipBuilder) { Consumer existingBuilder = this.tooltipBuilder; - if (existingBuilder != null) { + if (existingBuilder != null && tooltipBuilder != null) { this.tooltipBuilder = tooltip -> { existingBuilder.accept(this); tooltipBuilder.accept(this);