Files
ReplayModCinematic/src/main/java/com/replaymod/replay/ReplayHandler.java
Jonas Herzig cfb9e15b8a Make use of new @Pattern feature to centralize version-aware code
That is, most of the business code should not be aware that it is being compiled
to multiple versions even when it heavily interacts with MC, preprocessor
statements should be an escape hatch, not the norm.
Similarly, code should not be forced to do `MCVer.getWindow(mc)` instead of the
much more intuitive `mc.getWindow()`, and a new preprocessor (technically remap)
feature makes this possible by defining "search and replace"-like patterns (but
smarter in that they are type-aware) in one or more central places (the
"Patterns.java" files) which then are applied all over the code base.

In a way, this is another step in the automatic back-porting process where
preprocessor statements are used when we cannot yet do something automatically.
Previously we "merely" automatically converted between different mapping, this
new feature now also allows us to automatically perform simple refactoring
tasks like changing field access to a getter+setter (e.g. `mc.getWindow()`), or
changing how a method is called (e.g. `BufferBuilder.begin`), or changing a
method call chain (e.g. `dispatcher.camera.getYaw()`), or most other
search-and-replace-like changes and any combination of those.
The only major limitation is that the replacement itself is not smart, so
arguments must be kept in same order (or be temporarily assigned to local
variables which then can be used in any order).
2021-03-14 12:12:51 +01:00

726 lines
26 KiB
Java

package com.replaymod.replay;
import com.google.common.base.Preconditions;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.replaymod.core.ReplayMod;
import com.replaymod.core.mixin.MinecraftAccessor;
import com.replaymod.core.mixin.TimerAccessor;
import com.replaymod.core.utils.Restrictions;
import com.replaymod.core.utils.Utils;
import com.replaymod.core.utils.WrappedTimer;
import com.replaymod.replay.camera.CameraEntity;
import com.replaymod.replay.camera.SpectatorCameraController;
import com.replaymod.replay.events.ReplayClosedCallback;
import com.replaymod.replay.events.ReplayClosingCallback;
import com.replaymod.replay.events.ReplayOpenedCallback;
import com.replaymod.replay.gui.overlay.GuiReplayOverlay;
import com.replaymod.replaystudio.data.Marker;
import com.replaymod.replaystudio.replay.ReplayFile;
import com.replaymod.replaystudio.util.Location;
import de.johni0702.minecraft.gui.container.AbstractGuiScreen;
import de.johni0702.minecraft.gui.container.GuiContainer;
import de.johni0702.minecraft.gui.container.GuiScreen;
import de.johni0702.minecraft.gui.element.GuiLabel;
import de.johni0702.minecraft.gui.element.advanced.GuiProgressBar;
import de.johni0702.minecraft.gui.layout.HorizontalLayout;
import de.johni0702.minecraft.gui.popup.AbstractGuiPopup;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.embedded.EmbeddedChannel;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.gui.screen.DownloadingTerrainScreen;
import net.minecraft.client.network.ClientLoginNetworkHandler;
import net.minecraft.client.util.Window;
import net.minecraft.util.crash.CrashReport;
import net.minecraft.entity.Entity;
import net.minecraft.entity.player.PlayerEntity;
import net.minecraft.network.ClientConnection;
import java.io.IOException;
import java.util.*;
//#if MC>=11600
import net.minecraft.client.util.math.MatrixStack;
//#endif
//#if MC>=11500
import com.mojang.blaze3d.systems.RenderSystem;
import org.lwjgl.opengl.GL11;
//#endif
//#if MC>=11400
import com.replaymod.replay.mixin.EntityLivingBaseAccessor;
import net.minecraft.entity.LivingEntity;
//#else
//$$ import com.replaymod.replay.mixin.EntityOtherPlayerMPAccessor;
//$$ import net.minecraft.client.entity.EntityOtherPlayerMP;
//$$ import org.lwjgl.opengl.Display;
//#endif
//#if MC>=11200
//#else
//$$ import io.netty.channel.ChannelOutboundHandlerAdapter;
//#endif
//#if MC<10904
//$$ import de.johni0702.minecraft.gui.element.GuiLabel;
//$$ import de.johni0702.minecraft.gui.popup.GuiInfoPopup;
//$$ import de.johni0702.minecraft.gui.utils.Colors;
//#endif
//#if MC>=10800
import net.minecraft.network.NetworkSide;
//#if MC>=11400
//#else
//#if MC>=11400
//$$ import net.minecraftforge.fml.network.NetworkHooks;
//#else
//$$ import com.mojang.authlib.GameProfile;
//$$ import net.minecraft.client.network.NetHandlerPlayClient;
//$$ import net.minecraftforge.fml.common.network.handshake.NetworkDispatcher;
//#endif
//#endif
//#else
//$$ import cpw.mods.fml.client.FMLClientHandler;
//$$ import cpw.mods.fml.common.Loader;
//$$ import cpw.mods.fml.common.network.internal.FMLNetworkHandler;
//$$ import com.replaymod.replay.gui.screen.GuiOpeningReplay;
//$$ import net.minecraft.entity.EntityLivingBase;
//$$
//$$ import java.net.InetSocketAddress;
//$$ import java.net.SocketAddress;
//#endif
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import static com.replaymod.core.versions.MCVer.*;
import static com.replaymod.replay.ReplayModReplay.LOGGER;
import static com.mojang.blaze3d.platform.GlStateManager.*;
import static org.lwjgl.opengl.GL11.GL_COLOR_BUFFER_BIT;
import static org.lwjgl.opengl.GL11.GL_DEPTH_BUFFER_BIT;
public class ReplayHandler {
private static MinecraftClient mc = getMinecraft();
/**
* The file currently being played.
*/
private final ReplayFile replayFile;
/**
* Decodes and sends packets into channel.
*/
private final FullReplaySender fullReplaySender;
//#if MC>=10904
private final QuickReplaySender quickReplaySender;
private boolean quickMode = false;
//#else
//$$ private static final String QUICK_MODE_MIN_MC = "1.9.4";
//#endif
/**
* Currently active replay restrictions.
*/
private Restrictions restrictions = new Restrictions();
/**
* Whether camera movements by user input and/or server packets should be suppressed.
*/
private boolean suppressCameraMovements;
private Set<Marker> markers;
private final GuiReplayOverlay overlay;
private EmbeddedChannel channel;
private int replayDuration;
/**
* The position at which the camera should be located after the next jump.
*/
private Location targetCameraPosition;
private UUID spectating;
public ReplayHandler(ReplayFile replayFile, boolean asyncMode) throws IOException {
Preconditions.checkState(mc.isOnThread(), "Must be called from Minecraft thread.");
this.replayFile = replayFile;
replayDuration = replayFile.getMetaData().getDuration();
markers = replayFile.getMarkers().or(Collections.emptySet());
fullReplaySender = new FullReplaySender(this, replayFile, false);
//#if MC>=10904
quickReplaySender = new QuickReplaySender(ReplayModReplay.instance, replayFile);
//#endif
setup();
overlay = new GuiReplayOverlay(this);
overlay.setVisible(true);
ReplayOpenedCallback.EVENT.invoker().replayOpened(this);
fullReplaySender.setAsyncMode(asyncMode);
}
void restartedReplay() {
Preconditions.checkState(mc.isOnThread(), "Must be called from Minecraft thread.");
channel.close();
//#if MC>=11400
mc.mouse.unlockCursor();
//#else
//$$ mc.setIngameNotInFocus();
//#endif
// Force re-creation of camera entity by unloading the previous world
//#if MC>=11400
mc.disconnect();
//#else
//$$ // We need to re-set the GUI screen because having one with `allowsUserInput = true` active during world
//$$ // load (i.e. before player is set) will crash MC...
//$$ mc.displayGuiScreen(new net.minecraft.client.gui.GuiScreen() {});
//$$ mc.loadWorld(null);
//#endif
restrictions = new Restrictions();
setup();
}
public void endReplay() throws IOException {
Preconditions.checkState(mc.isOnThread(), "Must be called from Minecraft thread.");
ReplayClosingCallback.EVENT.invoker().replayClosing(this);
fullReplaySender.terminateReplay();
//#if MC>=10904
if (quickMode) {
quickReplaySender.unregister();
}
//#endif
replayFile.save();
replayFile.close();
channel.close().awaitUninterruptibly();
if (mc.player instanceof CameraEntity) {
//#if MC>=11400
mc.player.remove();
//#else
//$$ mc.player.setDead();
//#endif
}
if (mc.world != null) {
//#if MC>=11400
mc.disconnect();
//#else
//$$ mc.world.sendQuittingDisconnectingPacket();
//$$ mc.loadWorld(null);
//#endif
}
TimerAccessor timer = (TimerAccessor) ((MinecraftAccessor) mc).getTimer();
//#if MC>=11200
timer.setTickLength(WrappedTimer.DEFAULT_MS_PER_TICK);
//#else
//$$ timer.setTimerSpeed(1);
//#endif
overlay.setVisible(false);
ReplayModReplay.instance.forcefullyStopReplay();
mc.openScreen(null);
ReplayClosedCallback.EVENT.invoker().replayClosed(this);
}
private void setup() {
Preconditions.checkState(mc.isOnThread(), "Must be called from Minecraft thread.");
//#if MC>=11100
mc.inGameHud.getChatHud().clear(false);
//#else
//$$ mc.ingameGUI.getChatGUI().clearChatMessages();
//#endif
//#if MC>=10800
ClientConnection networkManager = new ClientConnection(NetworkSide.CLIENTBOUND) {
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable t) {
t.printStackTrace();
}
};
//#else
//$$ NetworkManager networkManager = new NetworkManager(true) {
//$$ @Override
//$$ public SocketAddress getRemoteAddress() {
//$$ // See https://github.com/Dyonovan/TCNodeTracker/issues/37
//$$ if (Loader.isModLoaded("tcnodetracker")) {
//$$ StackTraceElement elem = Thread.currentThread().getStackTrace()[2];
//$$ if ("com.dyonovan.tcnodetracker.events.ClientConnectionEvent".equals(elem.getClassName())) {
//$$ LOGGER.debug("TCNodeTracker crash workaround applied");
//$$ return new InetSocketAddress("replaymod.dummy", 0);
//$$ }
//$$ }
//$$ return super.getRemoteAddress();
//$$ }
//$$
//$$ @Override
//$$ public void exceptionCaught(ChannelHandlerContext ctx, Throwable t) {
//$$ t.printStackTrace();
//$$ }
//$$ };
//$$ mc.displayGuiScreen(new GuiOpeningReplay(networkManager));
//$$ FMLClientHandler.instance().connectToRealmsServer(null, 0); // just to init the playClientBlock latch
//#endif
networkManager.setPacketListener(new ClientLoginNetworkHandler(
networkManager,
mc,
null
//#if MC>=11400
, it -> {}
//#endif
));
//#if MC>=11200
channel = new EmbeddedChannel();
//#else
//$$ ChannelOutboundHandlerAdapter dummyHandler = new ChannelOutboundHandlerAdapter();
//$$ channel = new EmbeddedChannel(dummyHandler);
//$$ channel.pipeline().remove(dummyHandler);
//#endif
//#if MC>=10904
channel.pipeline().addLast("ReplayModReplay_quickReplaySender", quickReplaySender);
//#endif
channel.pipeline().addLast("ReplayModReplay_replaySender", fullReplaySender);
channel.pipeline().addLast("packet_handler", networkManager);
channel.pipeline().fireChannelActive();
//#if MC>=11400 && MC<11400
//$$ NetworkHooks.registerClientLoginChannel(networkManager);
//#endif
}
public ReplayFile getReplayFile() {
return replayFile;
}
public Restrictions getRestrictions() {
return restrictions;
}
public ReplaySender getReplaySender() {
//#if MC>=10904
return quickMode ? quickReplaySender : fullReplaySender;
//#else
//$$ return fullReplaySender;
//#endif
}
public GuiReplayOverlay getOverlay() {
return overlay;
}
//#if MC>=10904
public void ensureQuickModeInitialized(Runnable andThen) {
if (Utils.ifMinimalModeDoPopup(overlay, () -> {})) return;
ListenableFuture<Void> future = quickReplaySender.getInitializationPromise();
if (future == null) {
InitializingQuickModePopup popup = new InitializingQuickModePopup(overlay);
future = quickReplaySender.initialize(progress -> popup.progressBar.setProgress(progress.floatValue()));
Futures.addCallback(future, new FutureCallback<Void>() {
@Override
public void onSuccess(@Nullable Void result) {
popup.close();
}
@Override
public void onFailure(@Nonnull Throwable t) {
String message = "Failed to initialize quick mode. It will not be available.";
Utils.error(LOGGER, overlay, CrashReport.create(t, message), popup::close);
}
});
}
Futures.addCallback(future, new FutureCallback<Void>() {
@Override
public void onSuccess(@Nullable Void result) {
andThen.run();
}
@Override
public void onFailure(@Nonnull Throwable t) {
// Exception already printed in callback added above
}
});
}
private class InitializingQuickModePopup extends AbstractGuiPopup<InitializingQuickModePopup> {
private final GuiProgressBar progressBar = new GuiProgressBar(popup).setSize(300, 20)
.setI18nLabel("replaymod.gui.loadquickmode");
public InitializingQuickModePopup(GuiContainer container) {
super(container);
open();
}
@Override
public void close() {
super.close();
}
@Override
protected InitializingQuickModePopup getThis() {
return this;
}
}
public void setQuickMode(boolean quickMode) {
if (ReplayMod.isMinimalMode()) {
throw new UnsupportedOperationException("Quick Mode not supported in minimal mode.");
}
if (quickMode == this.quickMode) return;
if (quickMode && fullReplaySender.isAsyncMode()) {
// If this method is called via runLater, then it cannot switch to sync mode by itself as there might be
// some rogue packets in the task queue after it. Instead the caller must switch to sync mode first and
// use runLater until all packets have been processed (when using setAsyncModeAndWait, one runLater should
// be sufficient).
throw new IllegalStateException("Cannot switch to quick mode while in async mode.");
}
this.quickMode = quickMode;
CameraEntity cam = getCameraEntity();
if (cam != null) {
targetCameraPosition = new Location(cam.getX(), cam.getY(), cam.getZ(), cam.yaw, cam.pitch);
} else {
targetCameraPosition = null;
}
if (quickMode) {
quickReplaySender.register();
quickReplaySender.restart();
quickReplaySender.sendPacketsTill(fullReplaySender.currentTimeStamp());
} else {
quickReplaySender.unregister();
fullReplaySender.sendPacketsTill(0);
fullReplaySender.sendPacketsTill(quickReplaySender.currentTimeStamp());
}
moveCameraToTargetPosition();
}
public boolean isQuickMode() {
return quickMode;
}
//#else
//$$ public void ensureQuickModeInitialized(@SuppressWarnings("unused") Runnable andThen) {
//$$ GuiInfoPopup.open(overlay,
//$$ new GuiLabel().setI18nText("replaymod.gui.noquickmode", QUICK_MODE_MIN_MC).setColor(Colors.BLACK));
//$$ }
//$$
//$$ public void setQuickMode(@SuppressWarnings("unused") boolean quickMode) {
//$$ throw new UnsupportedOperationException("Quick Mode not supported on this version.");
//$$ }
//$$
//$$ public boolean isQuickMode() {
//$$ return false;
//$$ }
//#endif
public int getReplayDuration() {
return replayDuration;
}
/**
* Return whether camera movement by user inputs and/or server packets should be suppressed.
* @return {@code true} if these kinds of movement should be suppressed
*/
public boolean shouldSuppressCameraMovements() {
return suppressCameraMovements;
}
/**
* Set whether camera movement by user inputs and/or server packets should be suppressed.
* @param suppressCameraMovements {@code true} to suppress these kinds of movement, {@code false} to allow them
*/
public void setSuppressCameraMovements(boolean suppressCameraMovements) {
this.suppressCameraMovements = suppressCameraMovements;
}
/**
* Spectate the specified entity.
* When the entity is {@code null} or the camera entity, the camera becomes the view entity.
* @param e The entity to spectate
*/
//#if MC>=10800
public void spectateEntity(Entity e) {
//#else
//$$ public void spectateEntity(EntityLivingBase e) {
//#endif
CameraEntity cameraEntity = getCameraEntity();
if (cameraEntity == null) {
return; // Cannot spectate if we have no camera
}
if (e == null || e == cameraEntity) {
spectating = null;
e = cameraEntity;
} else if (e instanceof PlayerEntity) {
spectating = e.getUuid();
}
if (e == cameraEntity) {
cameraEntity.setCameraController(ReplayModReplay.instance.createCameraController(cameraEntity));
} else {
cameraEntity.setCameraController(new SpectatorCameraController(cameraEntity));
}
if (mc.getCameraEntity() != e) {
mc.setCameraEntity(e);
cameraEntity.setCameraPosRot(e);
}
}
/**
* Set the camera as the view entity.
* This is equivalent to {@code spectateEntity(null)}.
*/
public void spectateCamera() {
spectateEntity(null);
}
/**
* Returns whether the current view entity is the camera entity.
* @return {@code true} if the camera is the view entity, {@code false} otherwise
*/
public boolean isCameraView() {
return mc.player instanceof CameraEntity && mc.player == mc.getCameraEntity();
}
/**
* Returns the camera entity.
* @return The camera entity or {@code null} if it does not yet exist
*/
public CameraEntity getCameraEntity() {
return mc.player instanceof CameraEntity ? (CameraEntity) mc.player : null;
}
public UUID getSpectatedUUID() {
return spectating;
}
public void moveCameraToTargetPosition() {
CameraEntity cam = getCameraEntity();
if (cam != null && targetCameraPosition != null) {
cam.setCameraPosRot(targetCameraPosition);
}
}
public void doJump(int targetTime, boolean retainCameraPosition) {
//#if MC>=10904
if (getReplaySender() == quickReplaySender) {
// Always round to full tick
targetTime = targetTime + targetTime % 50;
if (targetTime >= 50) {
// Jump to time of previous tick first
quickReplaySender.sendPacketsTill(targetTime - 50);
}
// Update all entity positions (especially prev/lastTick values)
for (Entity entity : mc.world.getEntities()) {
skipTeleportInterpolation(entity);
entity.lastRenderX = entity.prevX = entity.getX();
entity.lastRenderY = entity.prevY = entity.getY();
entity.lastRenderZ = entity.prevZ = entity.getZ();
entity.prevYaw = entity.yaw;
entity.prevPitch = entity.pitch;
}
// Run previous tick
//#if MC>=11400
mc.tick();
//#else
//$$ try {
//$$ mc.runTick();
//$$ } catch (IOException e) {
//$$ throw new RuntimeException(e);
//$$ }
//#endif
// Jump to target tick
quickReplaySender.sendPacketsTill(targetTime);
// Immediately apply player teleport interpolation
for (Entity entity : mc.world.getEntities()) {
skipTeleportInterpolation(entity);
}
return;
}
//#endif
FullReplaySender replaySender = fullReplaySender;
if (replaySender.isHurrying()) {
return; // When hurrying, no Timeline jumping etc. is possible
}
if (targetTime < replaySender.currentTimeStamp()) {
mc.openScreen(null);
}
if (retainCameraPosition) {
CameraEntity cam = getCameraEntity();
if (cam != null) {
targetCameraPosition = new Location(cam.getX(), cam.getY(), cam.getZ(),
cam.yaw, cam.pitch);
} else {
targetCameraPosition = null;
}
}
long diff = targetTime - (replaySender.isHurrying() ? replaySender.getDesiredTimestamp() : replaySender.currentTimeStamp());
if (diff != 0) {
if (diff > 0 && diff < 5000) { // Small difference and no time travel
replaySender.jumpToTime(targetTime);
} else { // We either have to restart the replay or send a significant amount of packets
// Render our please-wait-screen
GuiScreen guiScreen = new GuiScreen();
guiScreen.setBackground(AbstractGuiScreen.Background.DIRT);
guiScreen.addElements(new HorizontalLayout.Data(0.5),
new GuiLabel().setI18nText("replaymod.gui.pleasewait"));
// Make sure that the replaysender changes into sync mode
replaySender.setSyncModeAndWait();
// Perform the rendering using OpenGL
pushMatrix();
clear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT
//#if MC>=11400
, true
//#endif
);
enableTexture();
mc.getFramebuffer().beginWrite(true);
Window window = mc.getWindow();
//#if MC>=11500
RenderSystem.clear(256, MinecraftClient.IS_SYSTEM_MAC);
RenderSystem.matrixMode(GL11.GL_PROJECTION);
RenderSystem.loadIdentity();
RenderSystem.ortho(0, window.getFramebufferWidth() / window.getScaleFactor(), window.getFramebufferHeight() / window.getScaleFactor(), 0, 1000, 3000);
RenderSystem.matrixMode(GL11.GL_MODELVIEW);
RenderSystem.loadIdentity();
RenderSystem.translatef(0, 0, -2000);
//#else
//#if MC>=11400
//$$ window.method_4493(true);
//#else
//$$ mc.entityRenderer.setupOverlayRendering();
//#endif
//#endif
guiScreen.toMinecraft().init(mc, window.getScaledWidth(), window.getScaledHeight());
//#if MC>=11600
guiScreen.toMinecraft().render(new MatrixStack(), 0, 0, 0);
//#else
//#if MC>=11400
//$$ guiScreen.toMinecraft().render(0, 0, 0);
//#else
//$$ guiScreen.toMinecraft().drawScreen(0, 0, 0);
//#endif
//#endif
guiScreen.toMinecraft().removed();
mc.getFramebuffer().endWrite();
popMatrix();
pushMatrix();
mc.getFramebuffer().draw(mc.getWindow().getFramebufferWidth(), mc.getWindow().getFramebufferHeight());
popMatrix();
//#if MC>=11500
mc.getWindow().swapBuffers();
//#else
//#if MC>=11400
//$$ mc.window.setFullscreen(true);
//#else
//$$ Display.update();
//#endif
//#endif
// Send the packets
do {
replaySender.sendPacketsTill(targetTime);
targetTime += 500;
} while (mc.player == null || mc.currentScreen instanceof DownloadingTerrainScreen);
replaySender.setAsyncMode(true);
replaySender.setReplaySpeed(0);
//#if MC<10800
//$$ while (mc.currentScreen instanceof GuiOpeningReplay) {
//$$ mc.currentScreen.handleInput();
//$$ }
//#endif
mc.getNetworkHandler().getConnection()
//#if MC>=11400
.tick();
//#else
//$$ .processReceivedPackets();
//#endif
for (Entity entity : mc.world.getEntities()) {
skipTeleportInterpolation(entity);
entity.lastRenderX = entity.prevX = entity.getX();
entity.lastRenderY = entity.prevY = entity.getY();
entity.lastRenderZ = entity.prevZ = entity.getZ();
entity.prevYaw = entity.yaw;
entity.prevPitch = entity.pitch;
}
//#if MC>=10800 && MC<11400
//$$ try {
//$$ mc.runTick();
//$$ } catch (IOException e) {
//$$ e.printStackTrace(); // This should never be thrown but whatever
//$$ }
//#else
mc.tick();
//#endif
//finally, updating the camera's position (which is not done by the sync jumping)
moveCameraToTargetPosition();
// No need to remove our please-wait-screen. It'll vanish with the next
// render pass as it's never been a real GuiScreen in the first place.
}
}
}
private void skipTeleportInterpolation(Entity entity) {
//#if MC>=11400
if (entity instanceof LivingEntity && !(entity instanceof CameraEntity)) {
LivingEntity e = (LivingEntity) entity;
EntityLivingBaseAccessor ea = (EntityLivingBaseAccessor) e;
e.updatePosition(ea.getInterpTargetX(), ea.getInterpTargetY(), ea.getInterpTargetZ());
e.yaw = (float) ea.getInterpTargetYaw();
e.pitch = (float) ea.getInterpTargetPitch();
}
//#else
//$$ if (entity instanceof EntityOtherPlayerMP) {
//$$ EntityOtherPlayerMP e = (EntityOtherPlayerMP) entity;
//$$ EntityOtherPlayerMPAccessor ea = (EntityOtherPlayerMPAccessor) e;
//$$ e.setPosition(ea.getOtherPlayerMPX(), ea.getOtherPlayerMPY(), ea.getOtherPlayerMPZ());
//$$ e.rotationYaw = (float) ea.getOtherPlayerMPYaw();
//$$ e.rotationPitch = (float) ea.getOtherPlayerMPPitch();
//$$ }
//#endif
}
}