Cleanup rendering subsystem
This commit is contained in:
parent
c89e65882c
commit
7d15c5c73d
6 changed files with 324 additions and 748 deletions
|
@ -213,135 +213,6 @@ public final class RenderingSubsystem extends SubsystemClass {
|
||||||
Logger.error("Rendering error occurred: " + error);
|
Logger.error("Rendering error occurred: " + error);
|
||||||
}
|
}
|
||||||
|
|
||||||
// -----> APIs
|
|
||||||
/**
|
|
||||||
* Renders all windows once.
|
|
||||||
* To render all windows continuously, invoke
|
|
||||||
* {@link #runRenderLoop(Runnable)} instead.
|
|
||||||
*
|
|
||||||
* @return map of windows and their {@link Throwable}s
|
|
||||||
* @throws NotOnMainThreadException if not running on the main thread
|
|
||||||
* @since v1-alpha9
|
|
||||||
*/
|
|
||||||
public LinkedHashMap<@NotNull Window, @NotNull Throwable> renderWindows() throws NotOnMainThreadException {
|
|
||||||
// Ensure running on the main thread
|
|
||||||
if (!Miscellaneous.onMainThread())
|
|
||||||
throw new NotOnMainThreadException();
|
|
||||||
|
|
||||||
LinkedHashMap<@NotNull Window, @NotNull Throwable> throwables = new LinkedHashMap<>();
|
|
||||||
|
|
||||||
// Poll for events
|
|
||||||
glfwPollEvents();
|
|
||||||
|
|
||||||
// Update and render all windows
|
|
||||||
for (Window window : Window.getWindows()) {
|
|
||||||
if (!window.isRendering())
|
|
||||||
continue;
|
|
||||||
|
|
||||||
try {
|
|
||||||
window.updateState();
|
|
||||||
window.render();
|
|
||||||
} catch (Throwable throwable) {
|
|
||||||
Logger.error("Rendering window " + window + " failed: Threw throwable " + throwable.getClass().getName() + (throwable.getMessage() == null ? "" : ": " + throwable.getMessage()));
|
|
||||||
throwables.put(window, throwable);
|
|
||||||
}
|
|
||||||
|
|
||||||
//bgfx_frame(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
return throwables;
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* Renders all windows continuously.
|
|
||||||
* To render all windows just once, invoke
|
|
||||||
* {@link #renderWindows()} instead.
|
|
||||||
* <p>
|
|
||||||
* Immediately returns when a {@link #renderWindows()} call fails.
|
|
||||||
*
|
|
||||||
* @param frameCode code which shall be invoked before a frame is rendered
|
|
||||||
* @return see {@link #renderWindows()} (on failure)
|
|
||||||
* @throws NotOnMainThreadException if not running on the main thread
|
|
||||||
* @since v1-alpha9
|
|
||||||
*/
|
|
||||||
public LinkedHashMap<@NotNull Window, @NotNull Throwable> runRenderLoop(@NotNull Runnable frameCode) throws NotOnMainThreadException {
|
|
||||||
// Ensure running on the main thread
|
|
||||||
if (!Miscellaneous.onMainThread())
|
|
||||||
throw new NotOnMainThreadException();
|
|
||||||
|
|
||||||
// Define variables
|
|
||||||
AtomicReference<LinkedHashMap<@NotNull Window, @NotNull Throwable>> output = new AtomicReference<>(new LinkedHashMap<>()); // runRenderLoop output
|
|
||||||
long renderTime; // Amount of time spent rendering
|
|
||||||
long sleepDuration; // Time spent sleeping the thread
|
|
||||||
LinkedList<Long> splitDeltaTime = new LinkedList<>(); // Used for calculating the delta time (render time average over one second)
|
|
||||||
long reportDuration = System.currentTimeMillis() + 1000; // Used for determining when to report frame count and delta time
|
|
||||||
double deltaTime; // Contains the average render time over one second (delta time)
|
|
||||||
|
|
||||||
// Check if delta time and frame count shall be printed to console.
|
|
||||||
// Unless this code is ran 292 billion years into the future,
|
|
||||||
// this should sufficiently disable the reporting feature.
|
|
||||||
if (!RenderingSubsystemConfiguration.getInstance().isDebugFrames())
|
|
||||||
reportDuration = Long.MAX_VALUE;
|
|
||||||
|
|
||||||
Logger.info("Entering render loop");
|
|
||||||
|
|
||||||
// Run while the 'output' is empty
|
|
||||||
while (output.get().isEmpty()) {
|
|
||||||
renderTime = Miscellaneous.measureExecutionTime(() -> {
|
|
||||||
output.set(renderWindows());
|
|
||||||
frameCode.run();
|
|
||||||
});
|
|
||||||
|
|
||||||
if (RenderingSubsystemConfiguration.getInstance().getVsyncMode() != VsyncMode.OFF)
|
|
||||||
// V-Sync is enabled, no need for manual busy waiting
|
|
||||||
sleepDuration = 0L;
|
|
||||||
else
|
|
||||||
// Calculate amount of time the thread should spend sleeping
|
|
||||||
sleepDuration = (long) (1d / RenderingSubsystemConfiguration.getInstance().getMaximumFramesPerSecond() * 1000d) - renderTime;
|
|
||||||
// Add render and sleep time to list used for calculating the delta time value
|
|
||||||
splitDeltaTime.add(renderTime + sleepDuration);
|
|
||||||
|
|
||||||
// Busy wait unless V-Sync is enabled
|
|
||||||
if (RenderingSubsystemConfiguration.getInstance().getVsyncMode() == VsyncMode.OFF && RenderingSubsystemConfiguration.getInstance().getMaximumFramesPerSecond() >= 1) {
|
|
||||||
sleepDuration += System.currentTimeMillis();
|
|
||||||
while (System.currentTimeMillis() < sleepDuration)
|
|
||||||
Thread.onSpinWait();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// Calculate delta time and frame count every second
|
|
||||||
if (System.currentTimeMillis() >= reportDuration) {
|
|
||||||
deltaTime = NumberUtil.calculateMeanLong(splitDeltaTime); // Calculate delta time
|
|
||||||
Logger.diag("Delta time average: " + deltaTime + " | Frames/s: " + 1000 / deltaTime); // Print delta time and frame count to console
|
|
||||||
|
|
||||||
if (RenderingSubsystemConfiguration.getInstance().isDebugWindowStates())
|
|
||||||
for (Window window : Window.getWindows())
|
|
||||||
Logger.diag(
|
|
||||||
"Window state for " + window.getUniqueIdentifier() + "\n" +
|
|
||||||
"-> Terminated: " + window.isTerminated() + "\n" +
|
|
||||||
"-> Name: " + window.getName() + "\n" +
|
|
||||||
"-> Title: " + window.getTitle() + "\n" +
|
|
||||||
"-> Size: " + window.getSize() + "\n" +
|
|
||||||
" -> Minimum: " + window.getMinimumSize() + "\n" +
|
|
||||||
" -> Maximum: " + window.getMaximumSize() + "\n" +
|
|
||||||
"-> Position: " + window.getPosition() + "\n" +
|
|
||||||
"-> Mode: " + window.getMode() + "\n" +
|
|
||||||
"-> Monitor: " + window.getMonitor() + "\n" +
|
|
||||||
"-> Resizable: " + window.isResizable() + "\n" +
|
|
||||||
"-> Borderless: " + window.isBorderless() + "\n" +
|
|
||||||
"-> Focusable: " + window.isFocusable() + "\n" +
|
|
||||||
"-> On top: " + window.isOnTop() + "\n" +
|
|
||||||
"-> Transparent: " + window.isTransparent() + "\n" +
|
|
||||||
"-> Rendering: " + window.isRendering()
|
|
||||||
);
|
|
||||||
|
|
||||||
reportDuration = System.currentTimeMillis() + 1000; // Update 'reportDuration'
|
|
||||||
splitDeltaTime.clear(); // Clear 'splitDeltaTime' list
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return output.get();
|
|
||||||
}
|
|
||||||
|
|
||||||
// -----> Utility methods
|
// -----> Utility methods
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -329,7 +329,7 @@ public final class RenderingSubsystemConfiguration extends Configuration {
|
||||||
debugInput = false;
|
debugInput = false;
|
||||||
debugFrames = true;
|
debugFrames = true;
|
||||||
debugWindowStates = true;
|
debugWindowStates = true;
|
||||||
debugAllowPositionUpdates = true;
|
debugAllowPositionUpdates = false;
|
||||||
|
|
||||||
initialPlatform = RenderingPlatform.ANY;
|
initialPlatform = RenderingPlatform.ANY;
|
||||||
initialDisableLibdecor = false;
|
initialDisableLibdecor = false;
|
||||||
|
@ -337,8 +337,8 @@ public final class RenderingSubsystemConfiguration extends Configuration {
|
||||||
errorRenderingFailures = true;
|
errorRenderingFailures = true;
|
||||||
|
|
||||||
renderingAdapter = RenderingAdapter.ANY;
|
renderingAdapter = RenderingAdapter.ANY;
|
||||||
vsyncMode = VsyncMode.ON;
|
vsyncMode = VsyncMode.OFF;
|
||||||
maximumFramesPerSecond = 60;
|
maximumFramesPerSecond = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** {@inheritDoc} */
|
/** {@inheritDoc} */
|
||||||
|
|
|
@ -25,7 +25,6 @@ import de.staropensource.engine.base.utility.misc.NumberUtil;
|
||||||
import de.staropensource.engine.rendering.RenderingSubsystemConfiguration;
|
import de.staropensource.engine.rendering.RenderingSubsystemConfiguration;
|
||||||
import de.staropensource.engine.rendering.exception.NotOnMainThreadException;
|
import de.staropensource.engine.rendering.exception.NotOnMainThreadException;
|
||||||
import de.staropensource.engine.rendering.type.FrameHandler;
|
import de.staropensource.engine.rendering.type.FrameHandler;
|
||||||
import de.staropensource.engine.rendering.type.Monitor;
|
|
||||||
import de.staropensource.engine.rendering.type.Window;
|
import de.staropensource.engine.rendering.type.Window;
|
||||||
import de.staropensource.engine.rendering.type.window.VsyncMode;
|
import de.staropensource.engine.rendering.type.window.VsyncMode;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
|
@ -175,7 +174,7 @@ public final class Renderer {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
bgfx_set_view_clear(0, BGFX_CLEAR_COLOR | BGFX_CLEAR_DEPTH, 0x000000, 1.0f, 0);
|
bgfx_set_view_clear(0, BGFX_CLEAR_COLOR | BGFX_CLEAR_DEPTH, 0x00000000, 1.0f, 0);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
addFrameHandler(new FrameHandler() {
|
addFrameHandler(new FrameHandler() {
|
||||||
|
@ -290,7 +289,19 @@ public final class Renderer {
|
||||||
glfwPollEvents();
|
glfwPollEvents();
|
||||||
|
|
||||||
// Reset backbuffer
|
// Reset backbuffer
|
||||||
bgfx_reset(Window.getWindows().getFirst().getSize().getX(), Window.getWindows().getFirst().getSize().getY(), RenderingSubsystemConfiguration.getInstance().getVsyncMode() == VsyncMode.ON ? BGFX_RESET_VSYNC : BGFX_RESET_NONE, BGFX_TEXTURE_FORMAT_RGBA4);
|
int resetSettings = 0;
|
||||||
|
if (RenderingSubsystemConfiguration.getInstance().getVsyncMode() == VsyncMode.ON)
|
||||||
|
resetSettings |= BGFX_RESET_TRANSPARENT_BACKBUFFER;
|
||||||
|
for (Window window : Window.getWindows())
|
||||||
|
if (window.isTransparent())
|
||||||
|
resetSettings |= BGFX_RESET_TRANSPARENT_BACKBUFFER;
|
||||||
|
|
||||||
|
bgfx_reset(
|
||||||
|
Window.getWindows().getFirst().getSize().getX(),
|
||||||
|
Window.getWindows().getFirst().getSize().getY(),
|
||||||
|
resetSettings,
|
||||||
|
BGFX_TEXTURE_FORMAT_RGBA4
|
||||||
|
);
|
||||||
|
|
||||||
// Render all windows
|
// Render all windows
|
||||||
for (Window window : Window.getWindows())
|
for (Window window : Window.getWindows())
|
||||||
|
@ -306,15 +317,9 @@ public final class Renderer {
|
||||||
|
|
||||||
// Determine time to wait for the next frame
|
// Determine time to wait for the next frame
|
||||||
execTimes.put("Waiting", 0L);
|
execTimes.put("Waiting", 0L);
|
||||||
switch (RenderingSubsystemConfiguration.getInstance().getVsyncMode()) {
|
if (RenderingSubsystemConfiguration.getInstance().getVsyncMode() == VsyncMode.OFF && RenderingSubsystemConfiguration.getInstance().getMaximumFramesPerSecond() > 0) {
|
||||||
case OFF -> execTimes.replace("Waiting", (long) (1d / RenderingSubsystemConfiguration.getInstance().getMaximumFramesPerSecond() * 1000d));
|
execTimes.replace("Waiting", (long) (1d / RenderingSubsystemConfiguration.getInstance().getMaximumFramesPerSecond() * 1000d));
|
||||||
case ON -> {
|
|
||||||
for (Monitor monitor : Monitor.getMonitors())
|
|
||||||
if (monitor.getRefreshRate() > execTimes.get("Waiting"))
|
|
||||||
execTimes.replace("Waiting", (long) monitor.getRefreshRate());
|
|
||||||
}
|
|
||||||
default -> {}
|
|
||||||
}
|
|
||||||
for (String time : execTimes.keySet())
|
for (String time : execTimes.keySet())
|
||||||
if (!time.equals("Waiting"))
|
if (!time.equals("Waiting"))
|
||||||
execTimes.replace("Waiting", execTimes.get("Waiting") - execTimes.get(time));
|
execTimes.replace("Waiting", execTimes.get("Waiting") - execTimes.get(time));
|
||||||
|
@ -326,6 +331,7 @@ public final class Renderer {
|
||||||
while (System.currentTimeMillis() < timesWait)
|
while (System.currentTimeMillis() < timesWait)
|
||||||
Thread.onSpinWait();
|
Thread.onSpinWait();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// Perform per-frame operations
|
// Perform per-frame operations
|
||||||
|
@ -358,10 +364,9 @@ public final class Renderer {
|
||||||
" -> Maximum: " + window.getMaximumSize() + "\n" +
|
" -> Maximum: " + window.getMaximumSize() + "\n" +
|
||||||
"-> Position: " + window.getPosition() + "\n" +
|
"-> Position: " + window.getPosition() + "\n" +
|
||||||
"-> Mode: " + window.getMode() + "\n" +
|
"-> Mode: " + window.getMode() + "\n" +
|
||||||
"-> Monitor: " + window.getMonitor() + "\n" +
|
|
||||||
"-> Resizable: " + window.isResizable() + "\n" +
|
"-> Resizable: " + window.isResizable() + "\n" +
|
||||||
"-> Borderless: " + window.isBorderless() + "\n" +
|
"-> Borderless: " + window.isBorderless() + "\n" +
|
||||||
"-> Focusable: " + window.isFocusable() + "\n" +
|
"-> Focused: " + window.isFocused() + "\n" +
|
||||||
"-> On top: " + window.isOnTop() + "\n" +
|
"-> On top: " + window.isOnTop() + "\n" +
|
||||||
"-> Transparent: " + window.isTransparent() + "\n" +
|
"-> Transparent: " + window.isTransparent() + "\n" +
|
||||||
"-> Rendering: " + window.isRendering()
|
"-> Rendering: " + window.isRendering()
|
||||||
|
|
|
@ -1,172 +0,0 @@
|
||||||
/*
|
|
||||||
* STAROPENSOURCE ENGINE SOURCE FILE
|
|
||||||
* Copyright (c) 2024 The StarOpenSource Engine Authors
|
|
||||||
* Licensed under the GNU Affero General Public License v3
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU Affero General Public License as
|
|
||||||
* published by the Free Software Foundation, either version 3 of the
|
|
||||||
* License, or (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU Affero General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU Affero General Public License
|
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package de.staropensource.engine.rendering.type;
|
|
||||||
|
|
||||||
import de.staropensource.engine.base.type.vector.Vec2i;
|
|
||||||
import de.staropensource.engine.rendering.exception.InvalidMonitorException;
|
|
||||||
import de.staropensource.engine.rendering.exception.NoMonitorsFoundException;
|
|
||||||
import lombok.Getter;
|
|
||||||
import org.jetbrains.annotations.NotNull;
|
|
||||||
import org.lwjgl.PointerBuffer;
|
|
||||||
import org.lwjgl.glfw.GLFWVidMode;
|
|
||||||
|
|
||||||
import java.util.LinkedHashSet;
|
|
||||||
import java.util.Objects;
|
|
||||||
import java.util.UUID;
|
|
||||||
|
|
||||||
import static org.lwjgl.glfw.GLFW.*;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Abstract class for implementing monitors in a windowing API.
|
|
||||||
* <p>
|
|
||||||
* Note that monitors stop working unannounced when disconnected,
|
|
||||||
* call {@link #isConnected()} before using to avoid unexpected behaviour.
|
|
||||||
*
|
|
||||||
* @since v1-alpha9
|
|
||||||
*/
|
|
||||||
@SuppressWarnings({ "JavadocDeclaration" })
|
|
||||||
public final class Monitor {
|
|
||||||
/**
|
|
||||||
* Contains the unique identifier.
|
|
||||||
* <p>
|
|
||||||
* This identifier is unique to every monitor and does not change during runtime.
|
|
||||||
*
|
|
||||||
* @since v1-alpha9
|
|
||||||
* -- GETTER --
|
|
||||||
* Returns the unique identifier.
|
|
||||||
* <p>
|
|
||||||
* This identifier is unique to every monitor and does not change during runtime.
|
|
||||||
*
|
|
||||||
* @return unique identifier
|
|
||||||
* @since v1-alpha9
|
|
||||||
*/
|
|
||||||
@Getter
|
|
||||||
private final UUID uniqueIdentifier = UUID.randomUUID();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Contains the monitor identifier.
|
|
||||||
* <p>
|
|
||||||
* This identifier is used by the windowing API to refer to a monitor and may change during runtime.
|
|
||||||
*
|
|
||||||
* @since v1-alpha9
|
|
||||||
* -- GETTER --
|
|
||||||
* Returns the monitor identifier.
|
|
||||||
* <p>
|
|
||||||
* This identifier is used by the windowing API to refer to a monitor and may change during runtime.
|
|
||||||
*
|
|
||||||
* @return monitor identifier
|
|
||||||
* @since v1-alpha9
|
|
||||||
* -- SETTER --
|
|
||||||
* Sets the monitor identifier.
|
|
||||||
* <p>
|
|
||||||
* This identifier is used by the windowing API to refer to a monitor and may change during runtime.
|
|
||||||
*
|
|
||||||
* @param identifier new monitor identifier
|
|
||||||
* @since v1-alpha9
|
|
||||||
*/
|
|
||||||
private final long identifier;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates and initializes an instance of this abstract class.
|
|
||||||
*
|
|
||||||
* @throws InvalidMonitorException if the monitor isn't connected
|
|
||||||
* @since v1-alpha9
|
|
||||||
*/
|
|
||||||
public Monitor(long identifier) throws InvalidMonitorException {
|
|
||||||
this.identifier = identifier;
|
|
||||||
|
|
||||||
checkConnected();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a set of all connected monitors.
|
|
||||||
*
|
|
||||||
* @return connected monitors
|
|
||||||
* @since v1-alpha9
|
|
||||||
*/
|
|
||||||
public static @NotNull LinkedHashSet<@NotNull Monitor> getMonitors() throws NoMonitorsFoundException {
|
|
||||||
PointerBuffer monitors = glfwGetMonitors();
|
|
||||||
LinkedHashSet<@NotNull Monitor> output = new LinkedHashSet<>();
|
|
||||||
if (monitors == null)
|
|
||||||
throw new NoMonitorsFoundException();
|
|
||||||
|
|
||||||
while (monitors.hasRemaining())
|
|
||||||
output.add(new Monitor(monitors.get()));
|
|
||||||
|
|
||||||
return output;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if this monitor is actually connected.
|
|
||||||
* If not, throws an {@link InvalidMonitorException}.
|
|
||||||
*
|
|
||||||
* @since v1-alpha9
|
|
||||||
*/
|
|
||||||
public void checkConnected() throws InvalidMonitorException, NoMonitorsFoundException {
|
|
||||||
if (!isConnected())
|
|
||||||
throw new InvalidMonitorException();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if this monitor is connected.
|
|
||||||
*
|
|
||||||
* @return connection status
|
|
||||||
* @since v1-alpha9
|
|
||||||
*/
|
|
||||||
public boolean isConnected() throws NoMonitorsFoundException {
|
|
||||||
return glfwGetMonitorName(identifier) != null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the name of this monitor.
|
|
||||||
*
|
|
||||||
* @return monitor name
|
|
||||||
* @since v1-alpha9
|
|
||||||
*/
|
|
||||||
public @NotNull String getName() throws InvalidMonitorException, NoMonitorsFoundException {
|
|
||||||
checkConnected();
|
|
||||||
return Objects.requireNonNull(glfwGetMonitorName(identifier));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns size of this monitor.
|
|
||||||
*
|
|
||||||
* @return monitor size
|
|
||||||
* @since v1-alpha9
|
|
||||||
*/
|
|
||||||
public @NotNull Vec2i getSize() throws InvalidMonitorException, NoMonitorsFoundException {
|
|
||||||
checkConnected();
|
|
||||||
|
|
||||||
GLFWVidMode videoMode = Objects.requireNonNull(glfwGetVideoMode(identifier));
|
|
||||||
|
|
||||||
return new Vec2i(videoMode.width(), videoMode.height());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns refresh rate of this monitor in hertz.
|
|
||||||
*
|
|
||||||
* @return monitor refresh rate
|
|
||||||
* @since v1-alpha9
|
|
||||||
*/
|
|
||||||
public short getRefreshRate() throws InvalidMonitorException, NoMonitorsFoundException {
|
|
||||||
checkConnected();
|
|
||||||
return (short) Objects.requireNonNull(glfwGetVideoMode(identifier)).refreshRate();
|
|
||||||
}
|
|
||||||
}
|
|
File diff suppressed because it is too large
Load diff
|
@ -42,10 +42,5 @@ public enum VsyncMode {
|
||||||
*
|
*
|
||||||
* @since v1-alpha9
|
* @since v1-alpha9
|
||||||
*/
|
*/
|
||||||
ON,
|
ON
|
||||||
|
|
||||||
/**
|
|
||||||
* Party
|
|
||||||
*/
|
|
||||||
PARTY
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue