Merge branch 'crashreports'

This commit is contained in:
OLEGSHA 2020-11-19 19:11:30 +03:00
commit a7eb90865f
27 changed files with 861 additions and 534 deletions

0
gradlew vendored Normal file → Executable file
View File

View File

@ -17,10 +17,23 @@
*******************************************************************************/
package ru.windcorp.progressia;
import ru.windcorp.progressia.common.util.crash.CrashReports;
import ru.windcorp.progressia.common.util.crash.analyzers.OutOfMemoryAnalyzer;
import ru.windcorp.progressia.common.util.crash.providers.OSContextProvider;
public class ProgressiaLauncher {
public static void launch(String[] args, Proxy proxy) {
setupCrashReports();
proxy.initialize();
}
private static void setupCrashReports() {
CrashReports.registerProvider(new OSContextProvider());
CrashReports.registerAnalyzer(new OutOfMemoryAnalyzer());
Thread.setDefaultUncaughtExceptionHandler((Thread thread, Throwable t)-> {
CrashReports.report(t,"Uncaught exception in thread %s", thread.getName());
});
}
}

View File

@ -26,6 +26,7 @@ import ru.windcorp.progressia.client.graphics.font.Typefaces;
import ru.windcorp.progressia.client.graphics.texture.Atlases;
import ru.windcorp.progressia.client.graphics.world.WorldRenderProgram;
import ru.windcorp.progressia.common.resource.ResourceManager;
import ru.windcorp.progressia.common.util.crash.CrashReports;
import ru.windcorp.progressia.server.ServerState;
import ru.windcorp.progressia.test.TestContent;
@ -39,8 +40,7 @@ public class ClientProxy implements Proxy {
RenderTaskQueue.waitAndInvoke(WorldRenderProgram::init);
RenderTaskQueue.waitAndInvoke(() -> Typefaces.setDefault(GNUUnifontLoader.load(ResourceManager.getResource("assets/unifont-13.0.03.hex.gz"))));
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
CrashReports.report(e, "ClientProxy failed");
}
TestContent.registerContent();

View File

@ -30,5 +30,5 @@ public class ProgressiaClientMain {
ProgressiaLauncher.launch(args, new ClientProxy());
}
}
}

View File

@ -1,6 +1,8 @@
package ru.windcorp.progressia.client.comms;
import java.io.IOException;
import ru.windcorp.jputil.chars.StringUtil;
import ru.windcorp.progressia.client.Client;
import ru.windcorp.progressia.client.graphics.world.EntityAnchor;
import ru.windcorp.progressia.client.world.ChunkRender;
@ -8,12 +10,13 @@ import ru.windcorp.progressia.common.comms.CommsListener;
import ru.windcorp.progressia.common.comms.packets.Packet;
import ru.windcorp.progressia.common.comms.packets.PacketSetLocalPlayer;
import ru.windcorp.progressia.common.comms.packets.PacketWorldChange;
import ru.windcorp.progressia.common.util.crash.CrashReports;
import ru.windcorp.progressia.common.world.entity.EntityData;
import ru.windcorp.progressia.common.world.entity.PacketEntityChange;
// TODO refactor with no mercy
public class DefaultClientCommsListener implements CommsListener {
private final Client client;
public DefaultClientCommsListener(Client client) {
@ -41,9 +44,13 @@ public class DefaultClientCommsListener implements CommsListener {
);
if (entity == null) {
throw new RuntimeException("");
CrashReports.report(
null,
"Player entity with ID %s not found",
new String(StringUtil.toFullHex(packet.getLocalPlayerEntityId()))
);
}
getClient().setLocalPlayer(entity);
getClient().getCamera().setAnchor(new EntityAnchor(
getClient().getWorld().getEntityRenderable(entity)
@ -58,7 +65,7 @@ public class DefaultClientCommsListener implements CommsListener {
public void onIOError(IOException reason) {
// TODO implement
}
public Client getClient() {
return client;
}

View File

@ -24,37 +24,38 @@ import ru.windcorp.progressia.client.graphics.backend.OpenGLObjectTracker;
import ru.windcorp.progressia.client.graphics.backend.OpenGLObjectTracker.OpenGLDeletable;
import ru.windcorp.progressia.client.graphics.backend.shaders.attributes.Attribute;
import ru.windcorp.progressia.client.graphics.backend.shaders.uniforms.Uniform;
import ru.windcorp.progressia.common.util.crash.CrashReports;
public class Program implements OpenGLDeletable {
private int handle;
public Program(Shader vertexShader, Shader fragmentShader) {
handle = glCreateProgram();
OpenGLObjectTracker.register(this);
glAttachShader(handle, vertexShader.getHandle());
glAttachShader(handle, fragmentShader.getHandle());
glLinkProgram(handle);
if (glGetProgrami(handle, GL_LINK_STATUS) == GL_FALSE) {
throw new RuntimeException("Bad program:\n" + glGetProgramInfoLog(handle));
CrashReports.report(null, "Bad program:\n%s", glGetProgramInfoLog(handle));
}
}
public Attribute getAttribute(String name) {
return new Attribute(glGetAttribLocation(handle, name), this);
}
public Uniform getUniform(String name) {
return new Uniform(glGetUniformLocation(handle, name), this);
}
public void use() {
glUseProgram(handle);
}
@Override
public void delete() {
glDeleteProgram(handle);

View File

@ -26,26 +26,26 @@ import ru.windcorp.progressia.client.graphics.backend.OpenGLObjectTracker;
import ru.windcorp.progressia.client.graphics.backend.OpenGLObjectTracker.OpenGLDeletable;
import ru.windcorp.progressia.common.resource.Resource;
import ru.windcorp.progressia.common.resource.ResourceManager;
import ru.windcorp.progressia.common.util.crash.CrashReports;
public class Shader implements OpenGLDeletable {
public static enum ShaderType {
VERTEX(GL_VERTEX_SHADER),
FRAGMENT(GL_FRAGMENT_SHADER);
VERTEX(GL_VERTEX_SHADER), FRAGMENT(GL_FRAGMENT_SHADER);
private final int glCode;
private ShaderType(int glCode) {
this.glCode = glCode;
}
public int getGlCode() {
return glCode;
}
public static ShaderType guessByResourceName(String resource) {
resource = resource.toLowerCase(Locale.ENGLISH);
if (resource.contains("vertex")) return VERTEX;
if (resource.contains("fragment")) return FRAGMENT;
if (resource.contains("vsh")) return VERTEX;
@ -57,48 +57,48 @@ public class Shader implements OpenGLDeletable {
);
}
}
private static final String SHADER_ASSETS_PREFIX = "assets/shaders/";
protected static Resource getShaderResource(String name) {
return ResourceManager.getResource(SHADER_ASSETS_PREFIX + name);
}
private final int handle;
private final ShaderType type;
public Shader(ShaderType type, String source) {
handle = glCreateShader(type.getGlCode());
OpenGLObjectTracker.register(this);
this.type = type;
glShaderSource(handle, source);
glCompileShader(handle);
if (glGetShaderi(handle, GL_COMPILE_STATUS) == GL_FALSE) {
System.out.println("***************** ERROR ******************");
System.out.println(source);
throw new RuntimeException("Bad shader:\n" + glGetShaderInfoLog(handle));
CrashReports.report(null, "Bad shader:\n %s", glGetShaderInfoLog(handle));
}
}
public Shader(String resource) {
this(
ShaderType.guessByResourceName(resource),
getShaderResource(resource).readAsString()
);
}
@Override
public void delete() {
glDeleteShader(handle);
}
public int getHandle() {
return handle;
}
public ShaderType getType() {
return type;
}

View File

@ -18,29 +18,30 @@
package ru.windcorp.progressia.client.graphics.backend.shaders.attributes;
import ru.windcorp.progressia.client.graphics.backend.shaders.Program;
import ru.windcorp.progressia.common.util.crash.CrashReports;
public class Attribute {
protected final int handle;
private final Program program;
public Attribute(int handle, Program program) {
if (handle < 0) {
throw new RuntimeException("Bad handle: " + handle);
CrashReports.report(null, "Bad handle: %d", handle);
}
this.handle = handle;
this.program = program;
}
public int getHandle() {
return handle;
}
public Program getProgram() {
return program;
}
public AttributeVertexArray asVertexArray() {
return new AttributeVertexArray(handle, program);
}

View File

@ -18,69 +18,70 @@
package ru.windcorp.progressia.client.graphics.backend.shaders.uniforms;
import ru.windcorp.progressia.client.graphics.backend.shaders.Program;
import ru.windcorp.progressia.common.util.crash.CrashReports;
public class Uniform {
protected final int handle;
private final Program program;
public Uniform(int handle, Program program) {
if (handle < 0) {
throw new RuntimeException("Bad handle: " + handle);
CrashReports.report(null, "Bad handle: %d", handle);
}
this.handle = handle;
this.program = program;
}
public int getHandle() {
return handle;
}
public Program getProgram() {
return program;
}
public Uniform1Float as1Float() {
return new Uniform1Float(handle, program);
}
public Uniform1Int as1Int() {
return new Uniform1Int(handle, program);
}
public Uniform2Float as2Float() {
return new Uniform2Float(handle, program);
}
public Uniform2Int as2Int() {
return new Uniform2Int(handle, program);
}
public Uniform3Float as3Float() {
return new Uniform3Float(handle, program);
}
public Uniform3Int as3Int() {
return new Uniform3Int(handle, program);
}
public Uniform4Float as4Float() {
return new Uniform4Float(handle, program);
}
public Uniform4Int as4Int() {
return new Uniform4Int(handle, program);
}
public Uniform2Matrix as2Matrix() {
return new Uniform2Matrix(handle, program);
}
public Uniform3Matrix as3Matrix() {
return new Uniform3Matrix(handle, program);
}
public Uniform4Matrix as4Matrix() {
return new Uniform4Matrix(handle, program);
}

View File

@ -21,152 +21,142 @@ import ru.windcorp.progressia.client.graphics.texture.Texture;
import ru.windcorp.progressia.client.graphics.texture.TextureDataEditor;
import ru.windcorp.progressia.client.graphics.texture.TextureSettings;
import ru.windcorp.progressia.common.resource.Resource;
import ru.windcorp.progressia.common.util.crash.CrashReports;
public class GNUUnifontLoader {
private static final AtlasGroup ATLAS_GROUP_GNU_UNIFONT =
new AtlasGroup("GNUUnifont", 1 << 12);
private static final TextureSettings TEXTURE_SETTINGS =
new TextureSettings(false);
private static final AtlasGroup ATLAS_GROUP_GNU_UNIFONT = new AtlasGroup("GNUUnifont", 1 << 12);
private static final TextureSettings TEXTURE_SETTINGS = new TextureSettings(false);
private static final int BITS_PER_HEX_DIGIT = 4;
private static final int PREFIX_LENGTH = "0000:".length();
private static class ParsedGlyph {
final char c;
final TextureDataEditor data;
ParsedGlyph(char c, TextureDataEditor data) {
this.c = c;
this.data = data;
}
}
private static class AtlasGlyph {
final char c;
final Texture texture;
AtlasGlyph(char c, Texture texture) {
this.c = c;
this.texture = texture;
}
}
public static GNUUnifont load(Resource resource) {
try (BufferedReader reader = createReader(resource)) {
return createStream(reader)
.map(GNUUnifontLoader::parse)
.map(GNUUnifontLoader::addToAtlas)
.collect(Collectors.collectingAndThen(
createMapper(),
GNUUnifont::new
));
return createStream(reader).map(GNUUnifontLoader::parse).map(GNUUnifontLoader::addToAtlas)
.collect(Collectors.collectingAndThen(createMapper(), GNUUnifont::new));
} catch (IOException | UncheckedIOException e) {
throw new RuntimeException(e);
CrashReports.report(e, "Could not load GNUUnifont");
return null;
}
}
private static BufferedReader createReader(Resource resource)
throws IOException
{
private static BufferedReader createReader(Resource resource) throws IOException {
return new BufferedReader(
new InputStreamReader(
new GZIPInputStream(
resource.getInputStream()
),
StandardCharsets.UTF_8
)
);
new InputStreamReader(new GZIPInputStream(resource.getInputStream()), StandardCharsets.UTF_8));
}
private static Stream<String> createStream(BufferedReader reader) {
return reader.lines();
}
private static ParsedGlyph parse(String declar) {
int width = getWidth(declar);
checkDeclaration(declar, width);
try {
int width = getWidth(declar);
checkDeclaration(declar, width);
char c = getChar(declar);
char c = getChar(declar);
TextureDataEditor editor = new TextureDataEditor(
width, GNUUnifont.HEIGHT,
width, GNUUnifont.HEIGHT,
TEXTURE_SETTINGS
);
TextureDataEditor editor = new TextureDataEditor(width, GNUUnifont.HEIGHT, width, GNUUnifont.HEIGHT,
TEXTURE_SETTINGS);
for (int y = 0; y < GNUUnifont.HEIGHT; ++y) {
for (int x = 0; x < width; ++x) {
int bit = x + y * width;
editor.setPixel(
x, GNUUnifont.HEIGHT - y - 1,
getBit(declar, bit) ? 0xFFFFFFFF : 0x00000000
);
for (int y = 0; y < GNUUnifont.HEIGHT; ++y) {
for (int x = 0; x < width; ++x) {
int bit = x + y * width;
editor.setPixel(x, GNUUnifont.HEIGHT - y - 1, getBit(declar, bit) ? 0xFFFFFFFF : 0x00000000);
}
}
}
return new ParsedGlyph(c, editor);
return new ParsedGlyph(c, editor);
} catch (IOException e) {
CrashReports.report(e, "Could not load GNUUnifont: could not load character \"%s\"", declar);
return null;
}
}
private static char getChar(String declar) {
int result = 0;
for (int i = 0; i < 4; ++i) {
result =
(result << BITS_PER_HEX_DIGIT) |
getHexValue(declar.charAt(i));
result = (result << BITS_PER_HEX_DIGIT) | getHexValue(declar.charAt(i));
}
return (char) result;
}
private static boolean getBit(String declar, int bit) {
int character = PREFIX_LENGTH + (bit / BITS_PER_HEX_DIGIT);
bit = bit % BITS_PER_HEX_DIGIT;
char c = declar.charAt(character);
int value = getHexValue(c);
return (value & (1 << (BITS_PER_HEX_DIGIT - bit - 1))) != 0;
}
private static int getWidth(String declar) {
int meaningfulChars = declar.length() - PREFIX_LENGTH;
final int charsPerColumn = GNUUnifont.HEIGHT / BITS_PER_HEX_DIGIT;
return meaningfulChars / charsPerColumn;
}
private static void checkDeclaration(String declar, int width) {
private static void checkDeclaration(String declar, int width) throws IOException {
if (!GNUUnifont.WIDTHS.contains(width)) {
throw new RuntimeException("Width " + width + " is not supported (in declar \"" + declar + "\")");
throw new IOException("Width " + width + " is not supported (in declar \"" + declar + "\")");
}
if ((declar.length() - PREFIX_LENGTH) % width != 0) {
throw new RuntimeException("Declar \"" + declar + "\" has invalid length");
throw new IOException("Declar \"" + declar + "\" has invalid length");
}
for (int i = 0; i < declar.length(); ++i) {
if (i == BITS_PER_HEX_DIGIT) {
if (declar.charAt(i) != ':') {
throw new RuntimeException("No colon ':' found in declar \"" + declar + "\" at index 4");
throw new IOException("No colon ':' found in declar \"" + declar + "\" at index 4");
}
} else {
char c = declar.charAt(i);
if (!((c >= '0' && c <= '9') || (c >= 'A' && c <= 'F'))) {
throw new RuntimeException("Illegal char in declar \"" + declar + "\" at index " + i + "; expected 0-9A-F");
throw new IOException("Illegal char in declar \"" + declar + "\" at index " + i + "; expected 0-9A-F");
}
}
}
}
private static int getHexValue(char digit) {
if (digit < '0') throw new NumberFormatException(digit + " is not a hex digit (0-9A-F expected)");
if (digit <= '9') return digit - '0';
if (digit < 'A') throw new NumberFormatException(digit + " is not a hex digit (0-9A-F expected)");
if (digit <= 'F') return digit - 'A' + 0xA;
if (digit < '0')
throw new NumberFormatException(digit + " is not a hex digit (0-9A-F expected)");
if (digit <= '9')
return digit - '0';
if (digit < 'A')
throw new NumberFormatException(digit + " is not a hex digit (0-9A-F expected)");
if (digit <= 'F')
return digit - 'A' + 0xA;
throw new NumberFormatException(digit + " is not a hex digit (0-9A-F expected)");
}
@ -174,24 +164,21 @@ public class GNUUnifontLoader {
Sprite sprite = Atlases.loadSprite(glyph.data.getData(), ATLAS_GROUP_GNU_UNIFONT);
return new AtlasGlyph(glyph.c, new SimpleTexture(sprite));
}
private static
Collector<AtlasGlyph, ?, TCharObjectMap<Texture>>
createMapper() {
return Collector.of(
TCharObjectHashMap<Texture>::new,
private static Collector<AtlasGlyph, ?, TCharObjectMap<Texture>> createMapper() {
return Collector.of(TCharObjectHashMap<Texture>::new,
(map, glyph) -> map.put(glyph.c, glyph.texture),
(a, b) -> {
a.putAll(b);
return a;
},
Characteristics.UNORDERED
);
Characteristics.UNORDERED);
}
private GNUUnifontLoader() {}
private GNUUnifontLoader() {
}
}

View File

@ -39,40 +39,40 @@ import ru.windcorp.progressia.client.graphics.input.bus.Input;
import ru.windcorp.progressia.client.graphics.input.bus.InputBus;
import ru.windcorp.progressia.client.graphics.input.bus.InputListener;
import ru.windcorp.progressia.common.util.Named;
import ru.windcorp.progressia.common.util.crash.CrashReports;
public class Component extends Named {
private final List<Component> children =
Collections.synchronizedList(new CopyOnWriteArrayList<>());
private final List<Component> children = Collections.synchronizedList(new CopyOnWriteArrayList<>());
private Component parent = null;
private EventBus eventBus = null;
private InputBus inputBus = null;
private int x, y;
private int width, height;
private boolean valid = false;
private Vec2i preferredSize = null;
private Object layoutHint = null;
private Layout layout = null;
private boolean isFocusable = false;
private boolean isFocused = false;
private boolean isHovered = false;
public Component(String name) {
super(name);
}
public Component getParent() {
return parent;
}
protected void setParent(Component parent) {
if (this.parent != parent) {
Component previousParent = this.parent;
@ -81,68 +81,71 @@ public class Component extends Named {
dispatchEvent(new ParentChangedEvent(this, previousParent, parent));
}
}
public List<Component> getChildren() {
return children;
}
public Component getChild(int index) {
synchronized (getChildren()) {
if (index < 0 || index >= getChildren().size()) return null;
if (index < 0 || index >= getChildren().size())
return null;
return getChildren().get(index);
}
}
public int getChildIndex(Component child) {
return getChildren().indexOf(child);
}
public int getOwnIndex() {
Component parent = getParent();
if (parent != null) {
return parent.getChildIndex(this);
}
return -1;
}
public void moveChild(Component child, int newIndex) {
if (newIndex == -1) newIndex = getChildren().size() - 1;
if (newIndex == -1)
newIndex = getChildren().size() - 1;
if (getChildren().remove(child)) {
getChildren().add(newIndex, child);
invalidate();
}
}
public void moveSelf(int newIndex) {
Component parent = getParent();
if (parent != null) {
parent.moveChild(this, newIndex);
}
}
public Component addChild(Component child, int index) {
if (index == -1) index = getChildren().size();
if (index == -1)
index = getChildren().size();
invalidate();
getChildren().add(index, child);
child.setParent(this);
dispatchEvent(new ChildAddedEvent(this, child));
return this;
}
public Component addChild(Component child) {
return addChild(child, -1);
}
public Component removeChild(Component child) {
if (!getChildren().contains(child)) {
return this;
}
if (child.isFocused()) {
child.focusNext();
}
@ -150,204 +153,205 @@ public class Component extends Named {
invalidate();
getChildren().remove(child);
child.setParent(null);
dispatchEvent(new ChildRemovedEvent(this, child));
return this;
}
public synchronized int getX() {
return x;
}
public synchronized int getY() {
return y;
}
public synchronized Component setPosition(int x, int y) {
invalidate();
this.x = x;
this.y = y;
return this;
}
public synchronized int getWidth() {
return width;
}
public synchronized int getHeight() {
return height;
}
public synchronized Component setSize(int width, int height) {
invalidate();
this.width = width;
this.height = height;
return this;
}
public Component setSize(Vec2i size) {
return setSize(size.x, size.y);
}
public synchronized Component setBounds(int x, int y, int width, int height) {
setPosition(x, y);
setSize(width, height);
return this;
}
public Component setBounds(int x, int y, Vec2i size) {
return setBounds(x, y, size.x, size.y);
}
public boolean isValid() {
return valid;
}
public synchronized void invalidate() {
valid = false;
getChildren().forEach(child -> child.invalidate());
}
public synchronized void validate() {
Component parent = getParent();
invalidate();
if (parent == null) {
layoutSelf();
} else {
parent.validate();
}
}
protected synchronized void layoutSelf() {
try {
if (getLayout() != null) {
getLayout().layout(this);
}
getChildren().forEach(child -> {
child.layoutSelf();
});
valid = true;
} catch (Exception e) {
throw new RuntimeException(e);
CrashReports.report(e, "Could not layout Component %s", this);
}
}
public synchronized Vec2i getPreferredSize() {
if (preferredSize != null) {
return preferredSize;
}
if (getLayout() != null) {
try {
return getLayout().calculatePreferredSize(this);
} catch (Exception e) {
throw new RuntimeException(e);
CrashReports.report(e, "Could not calculate preferred size for Component %s", this);
}
}
return new Vec2i(0, 0);
}
public synchronized Component setPreferredSize(Vec2i preferredSize) {
this.preferredSize = preferredSize;
return this;
}
public Component setPreferredSize(int width, int height) {
return setPreferredSize(new Vec2i(width, height));
}
public Layout getLayout() {
return layout;
}
public synchronized Component setLayout(Layout layout) {
invalidate();
this.layout = layout;
return this;
}
public Object getLayoutHint() {
return layoutHint;
}
public Component setLayoutHint(Object hint) {
this.layoutHint = hint;
return this;
}
public boolean isFocusable() {
return isFocusable;
}
public Component setFocusable(boolean focusable) {
this.isFocusable = focusable;
return this;
}
public boolean isFocused() {
return isFocused;
}
protected synchronized void setFocused(boolean focus) {
if (focus != this.isFocused) {
dispatchEvent(new FocusEvent(this, focus));
this.isFocused = focus;
}
}
public Component takeFocus() {
if (isFocused()) {
return this;
}
Component comp = this;
Component focused = null;
while (comp != null) {
if ((focused = comp.findFocused()) != null) {
focused.setFocused(false);
setFocused(true);
return this;
}
comp = comp.getParent();
}
setFocused(true);
return this;
}
public void focusNext() {
Component component = this;
while (true) {
component = component.getNextFocusCandidate(true);
if (component == this) {
return;
}
if (component.isFocusable()) {
setFocused(false);
component.setFocused(true);
return;
}
}
}
private Component getNextFocusCandidate(boolean canUseChildren) {
if (canUseChildren) synchronized (getChildren()) {
if (!getChildren().isEmpty()) {
return getChild(0);
if (canUseChildren)
synchronized (getChildren()) {
if (!getChildren().isEmpty()) {
return getChild(0);
}
}
}
Component parent = getParent();
if (parent != null) {
synchronized (parent.getChildren()) {
@ -356,32 +360,32 @@ public class Component extends Named {
return parent.getChild(ownIndex + 1);
}
}
return parent.getNextFocusCandidate(false);
}
return this;
}
public void focusPrevious() {
Component component = this;
while (true) {
component = component.getPreviousFocusCandidate();
if (component == this) {
return;
}
if (component.isFocusable()) {
setFocused(false);
component.setFocused(true);
return;
}
}
}
private Component getPreviousFocusCandidate() {
Component parent = getParent();
if (parent != null) {
@ -391,30 +395,30 @@ public class Component extends Named {
return parent.getChild(ownIndex - 1).getLastDeepChild();
}
}
return parent;
}
return getLastDeepChild();
}
private Component getLastDeepChild() {
synchronized (getChildren()) {
if (!getChildren().isEmpty()) {
return getChild(getChildren().size() - 1).getLastDeepChild();
}
return this;
}
}
public synchronized Component findFocused() {
if (isFocused()) {
return this;
}
Component result;
synchronized (getChildren()) {
for (Component c : getChildren()) {
result = c.findFocused();
@ -423,10 +427,10 @@ public class Component extends Named {
}
}
}
return null;
}
public boolean isHovered() {
return isHovered;
}
@ -434,9 +438,9 @@ public class Component extends Named {
protected void setHovered(boolean isHovered) {
if (this.isHovered != isHovered) {
this.isHovered = isHovered;
if (!isHovered && !getChildren().isEmpty()) {
getChildren().forEach(child -> {
if (child.isHovered()) {
child.setHovered(false);
@ -444,7 +448,7 @@ public class Component extends Named {
}
});
}
dispatchEvent(new HoverEvent(this, isHovered));
}
}
@ -453,40 +457,36 @@ public class Component extends Named {
if (eventBus == null) {
eventBus = new EventBus(getName());
}
eventBus.register(listener);
}
public void removeListener(Object listener) {
if (eventBus == null) return;
if (eventBus == null)
return;
eventBus.unregister(listener);
}
public void dispatchEvent(Object event) {
if (eventBus == null) return;
if (eventBus == null)
return;
eventBus.post(event);
}
public <T extends InputEvent> void addListener(
Class<? extends T> type,
boolean handlesConsumed,
InputListener<T> listener
) {
public <T extends InputEvent> void addListener(Class<? extends T> type, boolean handlesConsumed,
InputListener<T> listener) {
if (inputBus == null) {
inputBus = new InputBus();
}
inputBus.register(type, handlesConsumed, listener);
}
public <T extends InputEvent> void addListener(
Class<? extends T> type,
InputListener<T> listener
) {
public <T extends InputEvent> void addListener(Class<? extends T> type, InputListener<T> listener) {
if (inputBus == null) {
inputBus = new InputBus();
}
inputBus.register(type, listener);
}
@ -501,43 +501,45 @@ public class Component extends Named {
inputBus.dispatch(input);
}
}
public void dispatchInput(Input input) {
try {
switch (input.getTarget()) {
case FOCUSED:
dispatchInputToFocused(input);
break;
case HOVERED:
dispatchInputToHovered(input);
break;
case ALL:
default:
dispatchInputToAll(input);
break;
case FOCUSED:
dispatchInputToFocused(input);
break;
case HOVERED:
dispatchInputToHovered(input);
break;
case ALL:
default:
dispatchInputToAll(input);
break;
}
} catch (Exception e) {
throw new RuntimeException(e);
CrashReports.report(e, "Could not dispatch input to Component %s", this);
}
}
private void dispatchInputToFocused(Input input) {
Component c = findFocused();
if (c == null) return;
if (attemptFocusTransfer(input, c)) return;
if (c == null)
return;
if (attemptFocusTransfer(input, c))
return;
while (c != null) {
c.handleInput(input);
c = c.getParent();
}
}
private void dispatchInputToHovered(Input input) {
getChildren().forEach(child -> {
if (child.containsCursor()) {
child.setHovered(true);
if (!input.isConsumed()) {
child.dispatchInput(input);
}
@ -555,11 +557,13 @@ public class Component extends Named {
}
private boolean attemptFocusTransfer(Input input, Component focused) {
if (input.isConsumed()) return false;
if (!(input.getEvent() instanceof KeyEvent)) return false;
if (input.isConsumed())
return false;
if (!(input.getEvent() instanceof KeyEvent))
return false;
KeyEvent keyInput = (KeyEvent) input.getEvent();
if (keyInput.getKey() == GLFW.GLFW_KEY_TAB && !keyInput.isRelease()) {
input.consume();
if (keyInput.hasShift()) {
@ -569,23 +573,18 @@ public class Component extends Named {
}
return true;
}
return false;
}
public synchronized boolean contains(int x, int y) {
return
x >= getX() && x < getX() + getWidth() &&
y >= getY() && y < getY() + getHeight();
return x >= getX() && x < getX() + getWidth() && y >= getY() && y < getY() + getHeight();
}
public boolean containsCursor() {
return contains(
(int) InputTracker.getCursorX(),
(int) InputTracker.getCursorY()
);
return contains((int) InputTracker.getCursorX(), (int) InputTracker.getCursorY());
}
public void requestReassembly() {
if (parent != null) {
parent.requestReassembly();
@ -602,60 +601,60 @@ public class Component extends Named {
if (width == 0 || height == 0) {
return;
}
if (!isValid()) {
validate();
}
try {
assembleSelf(target);
} catch (Exception e) {
throw new RuntimeException(e);
CrashReports.report(e, "Could not assemble Component %s", this);
}
assembleChildren(target);
try {
postAssembleSelf(target);
} catch (Exception e) {
throw new RuntimeException(e);
CrashReports.report(e, "Post-assembly failed for Component %s", this);
}
}
protected void assembleSelf(RenderTarget target) {
// To be overridden
}
protected void postAssembleSelf(RenderTarget target) {
// To be overridden
}
protected void assembleChildren(RenderTarget target) {
getChildren().forEach(child -> child.assemble(target));
}
// /**
// * Returns a component that displays this component in its center.
// * @return a {@link Aligner} initialized to center this component
// */
// public Component center() {
// return new Aligner(this);
// }
//
// /**
// * Returns a component that aligns this component.
// * @return a {@link Aligner} initialized with this component
// */
// public Component align(double x, double y) {
// return new Aligner(this, x, y);
// }
//
// /**
// * Returns a component that allows scrolling this component
// * @return a {@link Scroller} initialized with this component
// */
// public Component scroller() {
// return new Scroller(this);
// }
// /**
// * Returns a component that displays this component in its center.
// * @return a {@link Aligner} initialized to center this component
// */
// public Component center() {
// return new Aligner(this);
// }
//
// /**
// * Returns a component that aligns this component.
// * @return a {@link Aligner} initialized with this component
// */
// public Component align(double x, double y) {
// return new Aligner(this, x, y);
// }
//
// /**
// * Returns a component that allows scrolling this component
// * @return a {@link Scroller} initialized with this component
// */
// public Component scroller() {
// return new Scroller(this);
// }
}

View File

@ -31,108 +31,101 @@ import gnu.trove.map.hash.TIntObjectHashMap;
import gnu.trove.map.hash.TObjectIntHashMap;
import gnu.trove.set.TIntSet;
import gnu.trove.set.hash.TIntHashSet;
import ru.windcorp.progressia.common.util.crash.CrashReports;
public class Keys {
private static final TIntObjectMap<String> CODES_TO_NAMES =
new TIntObjectHashMap<>();
private static final TObjectIntMap<String> NAMES_TO_CODES =
new TObjectIntHashMap<>();
private static final TIntObjectMap<String> CODES_TO_NAMES = new TIntObjectHashMap<>();
private static final TObjectIntMap<String> NAMES_TO_CODES = new TObjectIntHashMap<>();
private static final TIntSet MOUSE_BUTTONS = new TIntHashSet();
private static final String KEY_PREFIX = "GLFW_KEY_";
private static final String MOUSE_BUTTON_PREFIX = "GLFW_MOUSE_BUTTON_";
private static final Set<String> IGNORE_FIELDS =
new HashSet<>(Arrays.asList(
"GLFW_KEY_UNKNOWN",
"GLFW_KEY_LAST",
"GLFW_MOUSE_BUTTON_LAST",
"GLFW_MOUSE_BUTTON_1", // Alias for LEFT
private static final Set<String> IGNORE_FIELDS = new HashSet<>(
Arrays.asList("GLFW_KEY_UNKNOWN", "GLFW_KEY_LAST", "GLFW_MOUSE_BUTTON_LAST", "GLFW_MOUSE_BUTTON_1", // Alias
// for
// LEFT
"GLFW_MOUSE_BUTTON_2", // Alias for RIGHT
"GLFW_MOUSE_BUTTON_3" // Alias for MIDDLE
"GLFW_MOUSE_BUTTON_3" // Alias for MIDDLE
));
static {
initializeDictionary();
}
private static void initializeDictionary() {
try {
for (Field field : GLFW.class.getFields()) {
if (!Modifier.isStatic(field.getModifiers())) continue;
if (!Modifier.isFinal(field.getModifiers())) continue;
if (!Modifier.isStatic(field.getModifiers()))
continue;
if (!Modifier.isFinal(field.getModifiers()))
continue;
String name = field.getName();
if (
!name.startsWith(KEY_PREFIX) &&
!name.startsWith(MOUSE_BUTTON_PREFIX)
) continue;
if (IGNORE_FIELDS.contains(name)) continue;
if (!name.startsWith(KEY_PREFIX) && !name.startsWith(MOUSE_BUTTON_PREFIX))
continue;
if (IGNORE_FIELDS.contains(name))
continue;
addToDictionary(field);
}
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
CrashReports.report(e, "Cannot access GLFW constants");
}
}
private static void addToDictionary(Field field)
throws IllegalAccessException {
private static void addToDictionary(Field field) throws IllegalAccessException {
int value = field.getInt(null);
String name = field.getName();
if (name.startsWith(KEY_PREFIX)) {
name = name.substring(KEY_PREFIX.length());
} else if (name.startsWith(MOUSE_BUTTON_PREFIX)) {
name = "MOUSE_" + name.substring(MOUSE_BUTTON_PREFIX.length());
MOUSE_BUTTONS.add(value);
}
if (CODES_TO_NAMES.containsKey(value)) {
throw new RuntimeException(
"Duplicate keys: " + CODES_TO_NAMES.get(value) +
" and " + name + " both map to " + value +
"(0x" + Integer.toHexString(value) + ")"
);
CrashReports.report(null, "Duplicate keys: %s and %s both map to %d(0x%s)",
CODES_TO_NAMES.get(value), name, value, Integer.toHexString(value));
}
CODES_TO_NAMES.put(value, name);
NAMES_TO_CODES.put(name, value);
}
public static String getInternalName(int code) {
String result = CODES_TO_NAMES.get(code);
if (result == null) {
return "UNKNOWN";
}
return result;
}
public static String getDisplayName(int code) {
String name = getInternalName(code);
if (name.startsWith("KP_")) {
name = "KEYPAD_" + name.substring("KP_".length());
}
name = Character.toTitleCase(name.charAt(0)) +
name.substring(1).toLowerCase();
name = Character.toTitleCase(name.charAt(0)) + name.substring(1).toLowerCase();
name = name.replace('_', ' ');
return name;
}
public static int getCode(String internalName) {
if (NAMES_TO_CODES.containsKey(internalName)) {
return -1;
@ -140,7 +133,7 @@ public class Keys {
return NAMES_TO_CODES.get(internalName);
}
}
public static boolean isMouse(int code) {
return MOUSE_BUTTONS.contains(code);
}

View File

@ -13,72 +13,67 @@ import ru.windcorp.progressia.client.graphics.backend.RenderTaskQueue;
import ru.windcorp.progressia.common.resource.Resource;
import ru.windcorp.progressia.common.util.BinUtil;
import ru.windcorp.progressia.common.util.Named;
import ru.windcorp.progressia.common.util.crash.CrashReports;
public class Atlases {
public static class AtlasGroup extends Named {
private final int atlasSize;
public AtlasGroup(String name, int atlasSize) {
super(name);
this.atlasSize = atlasSize;
if (!BinUtil.isPowerOf2(atlasSize)) {
throw new IllegalArgumentException(
"Atlas size " + atlasSize
+ " is not a power of 2"
);
throw new IllegalArgumentException("Atlas size " + atlasSize + " is not a power of 2");
}
}
public int getAtlasSize() {
return atlasSize;
}
@Override
public int hashCode() {
return super.hashCode();
}
@Override
public boolean equals(Object obj) {
return super.equals(obj);
}
}
public static class Atlas {
private final AtlasGroup group;
private final TextureDataEditor editor;
private int nextX, nextY;
private int rowHeight;
private final TexturePrimitive primitive;
public Atlas(AtlasGroup group) {
this.group = group;
int size = group.getAtlasSize();
this.editor = new TextureDataEditor(size, size, size, size, SETTINGS);
this.primitive = new TexturePrimitive(editor.getData());
}
public Sprite addSprite(TextureData data) {
int width = data.getContentWidth();
int height = data.getContentHeight();
selectPosition(width, height);
editor.draw(data, nextX, nextY);
Sprite result = new Sprite(
getPrimitive(),
toPrimitiveCoords(nextX, nextY),
toPrimitiveCoords(width, height)
);
Sprite result = new Sprite(getPrimitive(), toPrimitiveCoords(nextX, nextY),
toPrimitiveCoords(width, height));
nextX += width;
return result;
}
@ -87,10 +82,10 @@ public class Atlases {
// Wrapping
nextY += rowHeight; // Move to next row
rowHeight = height; // Next row is at least 'height' high
nextX = 0; // Start the row over
nextX = 0; // Start the row over
} else {
// Not wrapping
// Update rowHeight if necessary
if (rowHeight < height) {
rowHeight = height;
@ -99,10 +94,7 @@ public class Atlases {
}
private Vec2 toPrimitiveCoords(int x, int y) {
return new Vec2(
toPrimitiveCoord(x),
toPrimitiveCoord(y)
);
return new Vec2(toPrimitiveCoord(x), toPrimitiveCoord(y));
}
private float toPrimitiveCoord(int c) {
@ -112,87 +104,82 @@ public class Atlases {
public boolean canAddSprite(TextureData data) {
int width = data.getContentWidth();
int height = data.getContentHeight();
// Try to fit without wrapping
if (nextY + height > getSize())
// Does not fit vertically
return false;
if (nextX + width <= getSize())
// Can place at (nextX; nextY)
return true;
// Try wrapping
if (width > getSize())
// GTFO. We couldn't fit if if we tried
return false;
if (nextY + rowHeight + height > getSize())
// Does not fit vertically when wrapped
return false;
// Can place at (0; nextY + rowHeight)
return true;
}
public AtlasGroup getGroup() {
return group;
}
public TexturePrimitive getPrimitive() {
return primitive;
}
public int getSize() {
return editor.getBufferWidth();
}
}
private static final TextureSettings SETTINGS = new TextureSettings(false);
private static final Map<Resource, Sprite> LOADED =
new HashMap<>();
private static final Multimap<AtlasGroup, Atlas> ATLASES =
MultimapBuilder.hashKeys().arrayListValues().build();
private static final Map<Resource, Sprite> LOADED = new HashMap<>();
private static final Multimap<AtlasGroup, Atlas> ATLASES = MultimapBuilder.hashKeys().arrayListValues().build();
public static Sprite getSprite(Resource resource, AtlasGroup group) {
return LOADED.computeIfAbsent(resource, k -> loadSprite(k, group));
}
private static Sprite loadSprite(Resource resource, AtlasGroup group) {
try {
TextureDataEditor data =
TextureLoader.loadPixels(resource, SETTINGS);
TextureDataEditor data = TextureLoader.loadPixels(resource, SETTINGS);
return loadSprite(data.getData(), group);
} catch (IOException e) {
throw new RuntimeException(e);
CrashReports.report(e, "Could not load sprite %s into atlas group %s", resource, group);
return null;
}
}
public static Sprite loadSprite(TextureData data, AtlasGroup group) {
Atlas atlas = getReadyAtlas(group, data);
return atlas.addSprite(data);
}
private static Atlas getReadyAtlas(AtlasGroup group, TextureData data) {
List<Atlas> atlases = (List<Atlas>) ATLASES.get(group);
if (
atlases.isEmpty() ||
!(atlases.get(atlases.size() - 1).canAddSprite(data))
) {
if (atlases.isEmpty() || !(atlases.get(atlases.size() - 1).canAddSprite(data))) {
Atlas newAtlas = new Atlas(group);
if (!newAtlas.canAddSprite(data)) {
throw new RuntimeException("Could not fit tex into atlas of size " + newAtlas.getSize());
CrashReports.report(null, "Could not fit texture into atlas of size %d", newAtlas.getSize());
}
atlases.add(newAtlas);
}
return atlases.get(atlases.size() - 1);
}
@ -201,7 +188,8 @@ public class Atlases {
RenderTaskQueue.invokeLater(atlas.getPrimitive()::load);
});
}
private Atlases() {}
private Atlases() {
}
}

View File

@ -6,21 +6,22 @@ import java.util.Map;
import ru.windcorp.progressia.common.resource.Resource;
import ru.windcorp.progressia.common.resource.ResourceManager;
import ru.windcorp.progressia.common.util.crash.CrashReports;
public class SimpleTextures {
private static final TextureSettings SETTINGS = new TextureSettings(false);
private static final Map<Resource, Texture> TEXTURES = new HashMap<>();
public static Texture get(Resource resource) {
return TEXTURES.computeIfAbsent(resource, SimpleTextures::load);
}
public static Texture get(String textureName) {
return get(ResourceManager.getTextureResource(textureName));
}
private static Texture load(Resource resource) {
try {
TextureDataEditor data =
@ -30,9 +31,10 @@ public class SimpleTextures {
new Sprite(new TexturePrimitive(data.getData()))
);
} catch (IOException e) {
throw new RuntimeException(e);
CrashReports.report(e, "Could not load texture %s", resource);
return null;
}
}
private SimpleTextures() {}

View File

@ -24,27 +24,28 @@ import java.util.Arrays;
import ru.windcorp.progressia.client.graphics.backend.OpenGLObjectTracker;
import ru.windcorp.progressia.client.graphics.backend.OpenGLObjectTracker.OpenGLDeletable;
import ru.windcorp.progressia.common.util.crash.CrashReports;
public class TexturePrimitive implements OpenGLDeletable {
private static final int NOT_LOADED = -1;
private static int[] currentlyBound = new int[32];
static {
Arrays.fill(currentlyBound, NOT_LOADED);
}
private int handle = NOT_LOADED;
private TextureData pixels;
public TexturePrimitive(TextureData pixels) {
this.pixels = pixels;
}
public TextureData getData() {
return pixels;
}
public int getBufferWidth() {
return pixels.getBufferWidth();
}
@ -64,21 +65,21 @@ public class TexturePrimitive implements OpenGLDeletable {
public boolean isLoaded() {
return handle != NOT_LOADED;
}
public void bind(int slot) {
if (!isLoaded()) {
load();
}
if (currentlyBound[slot] == handle) {
return;
}
int code = GL_TEXTURE0 + slot;
glActiveTexture(code);
glBindTexture(GL_TEXTURE_2D, handle);
currentlyBound[slot] = handle;
}
@ -87,9 +88,9 @@ public class TexturePrimitive implements OpenGLDeletable {
handle = pixels.load();
OpenGLObjectTracker.register(this);
if (handle < 0) {
throw new RuntimeException("oops");
CrashReports.report(null, "Failed to allocate texture");
}
}

View File

@ -3,6 +3,8 @@ package ru.windcorp.progressia.client.localization;
import java.lang.ref.WeakReference;
import java.util.*;
import ru.windcorp.progressia.common.util.crash.CrashReports;
public class Localizer {
private static final Localizer INSTANCE = new Localizer("assets/languages/", "en-US");
@ -17,7 +19,7 @@ public class Localizer {
private final Collection<WeakReference<LocaleListener>> listeners =
Collections.synchronizedCollection(new LinkedList<>());
//lang list must be in the same folder as .lang files
// lang list must be in the same folder as .lang files
public Localizer(String langFolder) {
this.langFolder = langFolder;
this.langList = new Parser(langFolder + "lang_list.txt").parse();
@ -41,7 +43,7 @@ public class Localizer {
data = new Parser(langFolder + this.language + ".lang").parse();
pokeListeners(language);
} else {
throw new RuntimeException("Language not found: " + language);
CrashReports.report(null, "Language not found: %s", language);
}
}
@ -64,7 +66,7 @@ public class Localizer {
}
private void pokeListeners(String newLanguage) {
//TODO extract as weak bus listener class
// TODO extract as weak bus listener class
synchronized (listeners) {
Iterator<WeakReference<LocaleListener>> iterator = listeners.iterator();
while (iterator.hasNext()) {

View File

@ -3,6 +3,7 @@ package ru.windcorp.progressia.client.localization;
import ru.windcorp.jputil.chars.EscapeException;
import ru.windcorp.jputil.chars.Escaper;
import ru.windcorp.progressia.common.resource.ResourceManager;
import ru.windcorp.progressia.common.util.crash.CrashReports;
import java.io.IOException;
import java.io.Reader;
@ -54,7 +55,7 @@ public class Parser {
if (c == '=') {
String key = ESCAPER.escape(stringBuilder.toString());
stringBuilder.setLength(0);
rawData.read(); //skip a char
rawData.read(); // skip a char
while (true) {
code = rawData.read();
if (code == -1) {
@ -80,7 +81,8 @@ public class Parser {
}
} catch (IOException | EscapeException e) {
throw new RuntimeException(e);
CrashReports.report(e, "Could not parse language file %s", filePath);
return null;
}
return parsedData;
}

View File

@ -7,6 +7,7 @@ import ru.windcorp.progressia.client.graphics.texture.TexturePrimitive;
import ru.windcorp.progressia.client.graphics.texture.TextureSettings;
import ru.windcorp.progressia.common.resource.ResourceManager;
import ru.windcorp.progressia.common.util.namespaces.NamespacedInstanceRegistry;
import ru.windcorp.progressia.common.util.crash.CrashReports;
public class EntityRenderRegistry extends NamespacedInstanceRegistry<EntityRender> {
@ -28,7 +29,8 @@ public class EntityRenderRegistry extends NamespacedInstanceRegistry<EntityRende
).getData()
);
} catch (IOException e) {
throw new RuntimeException(e);
CrashReports.report(e, "Could not load entity texture %s", name);
return null;
}
}

View File

@ -30,50 +30,53 @@ import com.google.common.io.CharStreams;
import ru.windcorp.progressia.Progressia;
import ru.windcorp.progressia.common.util.Named;
import ru.windcorp.progressia.common.util.crash.CrashReports;
public class Resource extends Named {
public Resource(String name) {
super(name);
}
public InputStream getInputStream() {
// TODO Do proper resource lookup
return Progressia.class.getClassLoader().getResourceAsStream(getName());
}
public Reader getReader() {
return new InputStreamReader(getInputStream());
}
public String readAsString() {
try (Reader reader = getReader()) {
return CharStreams.toString(reader);
} catch (IOException e) {
throw new RuntimeException(e); // TODO handle gracefully
CrashReports.report(e, "Could not read resource %s as text", this);
return null;
}
}
public ByteBuffer readAsBytes(ByteBuffer output) {
byte[] byteArray;
try (InputStream stream = getInputStream()) {
byteArray = ByteStreams.toByteArray(stream);
} catch (IOException e) {
throw new RuntimeException(e); // TODO handle gracefully
CrashReports.report(e, "Could not read resource %s as bytes", this);
return null;
}
if (output == null || output.remaining() < byteArray.length) {
output = BufferUtils.createByteBuffer(byteArray.length);
}
int position = output.position();
output.put(byteArray);
output.limit(output.position());
output.position(position);
return output;
}
public ByteBuffer readAsBytes() {
return readAsBytes(null);
}

View File

@ -0,0 +1,32 @@
package ru.windcorp.progressia.common.util.crash;
/**
* A crash report utility that performs analysis of a problem during crash
* report generation and presents its conclusion to the user via the crash report.
* Unlike {@link ContextProvider}s, Analyzers are provided with the reported problem
* details.
* @see ContextProvider
* @author serega404
*/
public interface Analyzer {
/**
* Provides a human-readable string describing this analyzer's conclusion
* on the presented problem, or returns {@code null} if no conclusion
* could be made.
* @param throwable The reported throwable (may be {@code null})
* @param messageFormat A {@linkplain java.util.Formatter#syntax format string} of a
* human-readable description of the problem
* @param args The arguments for the format string
* @return a conclusion or {@code null}
*/
String analyze(Throwable throwable, String messageFormat, Object... args);
/**
* Returns this analyzer's human-readable name.
* It should be A String In Title Case With Spaces.
* @return this analyzer's name
*/
String getName();
}

View File

@ -0,0 +1,31 @@
package ru.windcorp.progressia.common.util.crash;
import java.util.Map;
/**
* A crash report utility that gathers information about game and system state
* when a crash occurs and presents it to the user via the crash report.
* ContextProviders are not aware of the nature of the problem, unlike {@link Analyzer}s.
* @see Analyzer
* @author serega404
*/
public interface ContextProvider {
/**
* Provides human-readable description of the state of the game and the system.
* This information is {@link Map#put(Object, Object) put} into the provided map
* as key-value pairs. Keys are the characteristic being described, such as "OS Name",
* and should be Strings In Title Case With Spaces.
* If this provider cannot provide any information at this moment, the map is not
* modified.
* @param output the map to append output to
*/
void provideContext(Map<String, String> output);
/**
* Returns this provider's human-readable name.
* It should be A String In Title Case With Spaces.
* @return this provider's name
*/
String getName();
}

View File

@ -0,0 +1,232 @@
package ru.windcorp.progressia.common.util.crash;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.core.util.StringBuilderWriter;
import ru.windcorp.jputil.chars.StringUtil;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.Writer;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.*;
public class CrashReports {
private CrashReports() {
}
private static final Path CRASH_REPORTS_PATH = Paths.get("crash-reports");
private static final Collection<ContextProvider> PROVIDERS = Collections.synchronizedCollection(new ArrayList<>());
private static final Collection<Analyzer> ANALYZERS = Collections.synchronizedCollection(new ArrayList<>());
private static final Logger LOGGER = LogManager.getLogger("crash");
/**
* <em>This method never returns.</em>
* <p>
* TODO document
*
* @param throwable
* @param messageFormat
* @param args
*/
public static void report(Throwable throwable, String messageFormat, Object... args) {
StringBuilder output = new StringBuilder();
try {
String.format(messageFormat, args);
} catch (IllegalFormatException e) {
messageFormat = StringUtil.replaceAll(messageFormat, "%", "%%");
if (args.length != 0) {
messageFormat += "\nArgs:";
for (Object arg : args) {
try {
messageFormat += " \"" + arg.toString() + "\"";
} catch (Throwable t) {
messageFormat += " exc: \"" + t.getClass().toString() + "\"";
}
}
args = new Object[0]; // clear args
}
messageFormat += "\nCould not format provided description";
}
appendContextProviders(output);
addSeparator(output);
if (appendAnalyzers(output, throwable, messageFormat, args)) {
addSeparator(output);
}
appendMessageFormat(output, messageFormat, args);
appendStackTrace(output, throwable);
export(output.toString());
System.exit(0);
}
private static void appendContextProviders(StringBuilder output) {
// Do a local copy to avoid deadlocks -OLEGSHA
ContextProvider[] localProvidersCopy = PROVIDERS.toArray(new ContextProvider[PROVIDERS.size()]);
for (ContextProvider provider : localProvidersCopy) {
if (provider == null)
continue;
addSeparator(output);
try {
Map<String, String> buf = new HashMap<>();
provider.provideContext(buf);
if (!buf.isEmpty()) {
output.append("Provider name: ").append(provider.getName()).append("\n");
for (Map.Entry<String, String> entry : buf.entrySet()) {
output.append(entry.getKey()).append(": ").append(entry.getValue()).append("\n");
}
}
} catch (Throwable t) {
output.append("\n");
String providerName;
try {
providerName = provider.getName();
} catch (Throwable t1) {
providerName = provider.getClass().getName();
}
output.append(providerName).append(" is broken").append("\n");
// ContextProvider is broken
}
}
}
private static boolean appendAnalyzers(StringBuilder output, Throwable throwable, String messageFormat,
Object[] args) {
boolean analyzerResponsesExist = false;
// Do a local copy to avoid deadlocks -OLEGSHA
Analyzer[] localAnalyzersCopy = ANALYZERS.toArray(new Analyzer[ANALYZERS.size()]);
for (Analyzer analyzer : localAnalyzersCopy) {
if (analyzer == null)
continue;
String answer;
try {
answer = analyzer.analyze(throwable, messageFormat, args);
if (answer != null && !answer.isEmpty()) {
analyzerResponsesExist = true;
output.append(analyzer.getName()).append(": ").append(answer).append("\n");
}
} catch (Throwable t) {
analyzerResponsesExist = true;
output.append("\n");
String analyzerName;
try {
analyzerName = analyzer.getName();
} catch (Throwable t1) {
analyzerName = analyzer.getClass().getName();
}
output.append(analyzerName).append(" is broken").append("\n");
// Analyzer is broken
}
}
return analyzerResponsesExist;
}
private static void appendMessageFormat(StringBuilder output, String messageFormat, Object... arg) {
output.append("Provided description: \n").append(String.format(messageFormat, arg)).append("\n");
addSeparator(output);
}
private static void appendStackTrace(StringBuilder output, Throwable throwable) {
output.append("Stacktrace: \n");
if (throwable == null) {
output.append("no Throwable provided").append("\n");
return;
}
// Formatting to a human-readable string
Writer sink = new StringBuilderWriter(output);
try {
throwable.printStackTrace(new PrintWriter(sink));
} catch (Exception e) {
// PLAK
}
output.append("\n");
}
private static void export(String report) {
try {
LOGGER.fatal("/n" + report);
} catch (Exception e) {
// PLAK
}
System.err.println(report);
generateCrashReportFiles(report);
}
private static void generateCrashReportFiles(String output) {
Date date = new Date();
DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd-HH.mm.ss");
try {
if (!Files.exists(CRASH_REPORTS_PATH))
Files.createDirectory(CRASH_REPORTS_PATH);
createFileForCrashReport(output, CRASH_REPORTS_PATH.toString() + "/latest.log");
createFileForCrashReport(output,
CRASH_REPORTS_PATH.toString() + "/crash-" + dateFormat.format(date) + ".log");
} catch (Throwable t) {
// Crash Report not created
}
}
private static void createFileForCrashReport(String buffer, String filename) {
try (BufferedWriter writer = Files.newBufferedWriter(Paths.get(filename), StandardCharsets.UTF_8)) {
writer.write(buffer);
} catch (IOException ex) {
// Crash Report not created
}
}
public static void registerProvider(ContextProvider provider) {
PROVIDERS.add(provider);
}
public static void registerAnalyzer(Analyzer analyzer) {
ANALYZERS.add(analyzer);
}
private static void addSeparator(StringBuilder sb) {
sb.append(
// 80 chars
"--------------------------------------------------------------------------------").append("\n");
}
}

View File

@ -0,0 +1,17 @@
package ru.windcorp.progressia.common.util.crash.analyzers;
import ru.windcorp.progressia.common.util.crash.Analyzer;
public class OutOfMemoryAnalyzer implements Analyzer {
@Override
public String analyze(Throwable throwable, String messageFormat, Object... args) {
if (throwable instanceof OutOfMemoryError)
return "Try to add memory to the JVM";
return null;
}
@Override
public String getName() {
return "Out Of Memory Analyzer";
}
}

View File

@ -0,0 +1,20 @@
package ru.windcorp.progressia.common.util.crash.providers;
import ru.windcorp.progressia.common.util.crash.ContextProvider;
import java.util.Map;
public class OSContextProvider implements ContextProvider {
@Override
public void provideContext(Map<String, String> output) {
output.put("OS Name", System.getProperty("os.name"));
output.put("OS Version", System.getProperty("os.version"));
output.put("OS Architecture", System.getProperty("os.arch"));
}
@Override
public String getName() {
return "OS Context Provider";
}
}

View File

@ -7,10 +7,11 @@ import java.io.IOException;
import ru.windcorp.progressia.common.comms.packets.PacketWorldChange;
import ru.windcorp.progressia.common.state.IOContext;
import ru.windcorp.progressia.common.util.DataBuffer;
import ru.windcorp.progressia.common.util.crash.CrashReports;
import ru.windcorp.progressia.common.world.WorldData;
public class PacketEntityChange extends PacketWorldChange {
private long entityId;
private final DataBuffer buffer = new DataBuffer();
@ -21,15 +22,15 @@ public class PacketEntityChange extends PacketWorldChange {
protected PacketEntityChange(String id) {
super(id);
}
public long getEntityId() {
return entityId;
}
public void setEntityId(long entityId) {
this.entityId = entityId;
}
public DataBuffer getBuffer() {
return buffer;
}
@ -45,19 +46,15 @@ public class PacketEntityChange extends PacketWorldChange {
@Override
public void apply(WorldData world) {
EntityData entity = world.getEntity(getEntityId());
if (entity == null) {
throw new RuntimeException(
"Entity with ID " + getEntityId() + " not found"
);
CrashReports.report(null, "Entity with ID %d not found", getEntityId());
}
try {
entity.read(getReader(), IOContext.COMMS);
} catch (IOException e) {
throw new RuntimeException(
"Entity could not be read", e
);
CrashReports.report(e, "Entity could not be read");
}
}

View File

@ -11,6 +11,7 @@ import ru.windcorp.progressia.common.comms.packets.PacketWorldChange;
import ru.windcorp.progressia.common.state.IOContext;
import ru.windcorp.progressia.common.util.LowOverheadCache;
import ru.windcorp.progressia.common.util.Vectors;
import ru.windcorp.progressia.common.util.crash.CrashReports;
import ru.windcorp.progressia.common.world.Coordinates;
import ru.windcorp.progressia.common.world.WorldData;
import ru.windcorp.progressia.common.world.block.BlockData;
@ -21,19 +22,19 @@ import ru.windcorp.progressia.common.world.tile.TileData;
import ru.windcorp.progressia.server.Server;
public class ImplementedChangeTracker implements Changer {
public static interface ChangeImplementation {
void applyOnServer(WorldData world);
Packet asPacket();
}
private static class SetBlock
extends PacketWorldChange
implements ChangeImplementation {
private final Vec3i position = new Vec3i();
private BlockData block;
public SetBlock() {
this("Core:SetBlock");
}
@ -41,34 +42,34 @@ public class ImplementedChangeTracker implements Changer {
protected SetBlock(String id) {
super(id);
}
public void initialize(Vec3i position, BlockData block) {
this.position.set(position.x, position.y, position.z);
this.block = block;
}
@Override
public void applyOnServer(WorldData world) {
Vec3i blockInChunk = Vectors.grab3i();
Coordinates.convertInWorldToInChunk(position, blockInChunk);
world.getChunkByBlock(position).setBlock(blockInChunk, block);
Vectors.release(blockInChunk);
}
@Override
public void apply(WorldData world) {
applyOnServer(world);
}
@Override
public Packet asPacket() {
return this;
}
}
private static class AddOrRemoveTile
extends PacketWorldChange
implements ChangeImplementation {
@ -76,9 +77,9 @@ public class ImplementedChangeTracker implements Changer {
private final Vec3i position = new Vec3i();
private BlockFace face;
private TileData tile;
private boolean shouldAdd;
public AddOrRemoveTile() {
this("Core:AddOrRemoveTile");
}
@ -86,7 +87,7 @@ public class ImplementedChangeTracker implements Changer {
protected AddOrRemoveTile(String id) {
super(id);
}
public void initialize(
Vec3i position, BlockFace face,
TileData tile,
@ -97,52 +98,51 @@ public class ImplementedChangeTracker implements Changer {
this.tile = tile;
this.shouldAdd = shouldAdd;
}
@Override
public void applyOnServer(WorldData world) {
Vec3i blockInChunk = Vectors.grab3i();
Coordinates.convertInWorldToInChunk(position, blockInChunk);
List<TileData> tiles = world.getChunkByBlock(position)
.getTiles(blockInChunk, face);
List<TileData> tiles = world.getChunkByBlock(position).getTiles(blockInChunk, face);
if (shouldAdd) {
tiles.add(tile);
} else {
tiles.remove(tile);
}
Vectors.release(blockInChunk);
}
@Override
public void apply(WorldData world) {
applyOnServer(world);
}
@Override
public Packet asPacket() {
return this;
}
}
private static class ChangeEntity implements ChangeImplementation {
private EntityData entity;
private Change<?> change;
private final PacketEntityChange packet = new PacketEntityChange();
public <T extends EntityData> void set(T entity, Change<T> change) {
this.entity = entity;
this.change = change;
packet.setEntityId(entity.getEntityId());
try {
entity.write(packet.getWriter(), IOContext.COMMS);
} catch (IOException e) {
throw new RuntimeException(e);
CrashReports.report(e, "Could not write entity %s", entity);
}
}
@ -150,11 +150,11 @@ public class ImplementedChangeTracker implements Changer {
@Override
public void applyOnServer(WorldData world) {
((Change<EntityData>) change).change(entity);
try {
entity.write(packet.getWriter(), IOContext.COMMS);
} catch (IOException e) {
throw new RuntimeException("Entity could not be written", e);
CrashReports.report(e, "Could not write entity %s", entity);
}
}
@ -162,11 +162,11 @@ public class ImplementedChangeTracker implements Changer {
public Packet asPacket() {
return packet;
}
}
private final List<ChangeImplementation> changes = new ArrayList<>(1024);
private final LowOverheadCache<SetBlock> setBlockCache =
new LowOverheadCache<>(SetBlock::new);
@ -182,21 +182,21 @@ public class ImplementedChangeTracker implements Changer {
change.initialize(pos, block);
changes.add(change);
}
@Override
public void addTile(Vec3i block, BlockFace face, TileData tile) {
AddOrRemoveTile change = addOrRemoveTileCache.grab();
change.initialize(block, face, tile, true);
changes.add(change);
}
@Override
public void removeTile(Vec3i block, BlockFace face, TileData tile) {
AddOrRemoveTile change = addOrRemoveTileCache.grab();
change.initialize(block, face, tile, false);
changes.add(change);
}
@Override
public <T extends EntityData> void changeEntity(
T entity, Change<T> change
@ -205,7 +205,7 @@ public class ImplementedChangeTracker implements Changer {
changeRecord.set(entity, change);
changes.add(changeRecord);
}
public void applyChanges(Server server) {
changes.forEach(c -> c.applyOnServer(server.getWorld().getData()));
changes.stream().map(ChangeImplementation::asPacket).filter(Objects::nonNull).forEach(
@ -214,7 +214,7 @@ public class ImplementedChangeTracker implements Changer {
changes.forEach(this::release);
changes.clear();
}
private void release(ChangeImplementation c) {
if (c instanceof SetBlock) {
setBlockCache.release((SetBlock) c);

View File

@ -13,18 +13,14 @@
<RollingFile name="FileLog" fileName="${APP_LOG_ROOT}/game.log"
filePattern="${APP_LOG_ROOT}/game-%d{yyyy-MM-dd}-%i.log">
<LevelRangeFilter minLevel="FATAL" maxLevel="INFO" onMatch="ACCEPT" onMismatch="DENY"/>
<PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/>
<Policies>
<SizeBasedTriggeringPolicy size="18MB" />
</Policies>
<DefaultRolloverStrategy max="10"/>
</RollingFile>
</Appenders>
<Loggers>
<Root level="info">
<AppenderRef ref="FileLog" />
<AppenderRef ref="Console" />