diff options
Diffstat (limited to 'libraries/launcher/org/polymc')
-rw-r--r-- | libraries/launcher/org/polymc/EntryPoint.java | 164 | ||||
-rw-r--r-- | libraries/launcher/org/polymc/Launcher.java | 23 | ||||
-rw-r--r-- | libraries/launcher/org/polymc/LauncherFactory.java | 80 | ||||
-rw-r--r-- | libraries/launcher/org/polymc/applet/LegacyFrame.java | 162 | ||||
-rw-r--r-- | libraries/launcher/org/polymc/exception/ParameterNotFoundException.java | 25 | ||||
-rw-r--r-- | libraries/launcher/org/polymc/exception/ParseException.java | 25 | ||||
-rw-r--r-- | libraries/launcher/org/polymc/impl/OneSixLauncher.java | 189 | ||||
-rw-r--r-- | libraries/launcher/org/polymc/utils/Parameters.java | 78 | ||||
-rw-r--r-- | libraries/launcher/org/polymc/utils/Utils.java | 49 |
9 files changed, 795 insertions, 0 deletions
diff --git a/libraries/launcher/org/polymc/EntryPoint.java b/libraries/launcher/org/polymc/EntryPoint.java new file mode 100644 index 00000000..20f418eb --- /dev/null +++ b/libraries/launcher/org/polymc/EntryPoint.java @@ -0,0 +1,164 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 icelimetea, <fr3shtea@outlook.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * 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 General Public License for more details. + * + * Linking this library statically or dynamically with other modules is + * making a combined work based on this library. Thus, the terms and + * conditions of the GNU General Public License cover the whole + * combination. + * + * As a special exception, the copyright holders of this library give + * you permission to link this library with independent modules to + * produce an executable, regardless of the license terms of these + * independent modules, and to copy and distribute the resulting + * executable under terms of your choice, provided that you also meet, + * for each linked independent module, the terms and conditions of the + * license of that module. An independent module is a module which is + * not derived from or based on this library. If you modify this + * library, you may extend this exception to your version of the + * library, but you are not obliged to do so. If you do not wish to do + * so, delete this exception statement from your version. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.polymc; + +import org.polymc.exception.ParseException; +import org.polymc.utils.Parameters; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.util.logging.Level; +import java.util.logging.Logger; + +public final class EntryPoint { + + private static final Logger LOGGER = Logger.getLogger("EntryPoint"); + + private final Parameters params = new Parameters(); + + public static void main(String[] args) { + EntryPoint listener = new EntryPoint(); + + int retCode = listener.listen(); + + if (retCode != 0) { + LOGGER.info("Exiting with " + retCode); + + System.exit(retCode); + } + } + + private Action parseLine(String inData) throws ParseException { + String[] tokens = inData.split("\\s+", 2); + + if (tokens.length == 0) + throw new ParseException("Unexpected empty string!"); + + switch (tokens[0]) { + case "launch": { + return Action.Launch; + } + + case "abort": { + return Action.Abort; + } + + default: { + if (tokens.length != 2) + throw new ParseException("Error while parsing:" + inData); + + params.add(tokens[0], tokens[1]); + + return Action.Proceed; + } + } + } + + public int listen() { + Action action = Action.Proceed; + + try (BufferedReader reader = new BufferedReader(new InputStreamReader( + System.in, + StandardCharsets.UTF_8 + ))) { + String line; + + while (action == Action.Proceed) { + if ((line = reader.readLine()) != null) { + action = parseLine(line); + } else { + action = Action.Abort; + } + } + } catch (IOException | ParseException e) { + LOGGER.log(Level.SEVERE, "Launcher ABORT due to exception:", e); + + return 1; + } + + // Main loop + if (action == Action.Abort) { + LOGGER.info("Launch aborted by the launcher."); + + return 1; + } + + try { + Launcher launcher = + LauncherFactory + .getInstance() + .createLauncher(params); + + launcher.launch(); + + return 0; + } catch (IllegalArgumentException e) { + LOGGER.log(Level.SEVERE, "Wrong argument.", e); + + return 1; + } catch (Exception e) { + LOGGER.log(Level.SEVERE, "Exception caught from launcher.", e); + + return 1; + } + } + + private enum Action { + Proceed, + Launch, + Abort + } + +} diff --git a/libraries/launcher/org/polymc/Launcher.java b/libraries/launcher/org/polymc/Launcher.java new file mode 100644 index 00000000..5bff123e --- /dev/null +++ b/libraries/launcher/org/polymc/Launcher.java @@ -0,0 +1,23 @@ +/* + * Copyright 2012-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.polymc; + +public interface Launcher { + + void launch() throws Exception; + +} diff --git a/libraries/launcher/org/polymc/LauncherFactory.java b/libraries/launcher/org/polymc/LauncherFactory.java new file mode 100644 index 00000000..86862929 --- /dev/null +++ b/libraries/launcher/org/polymc/LauncherFactory.java @@ -0,0 +1,80 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 icelimetea, <fr3shtea@outlook.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * 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 General Public License for more details. + * + * Linking this library statically or dynamically with other modules is + * making a combined work based on this library. Thus, the terms and + * conditions of the GNU General Public License cover the whole + * combination. + * + * As a special exception, the copyright holders of this library give + * you permission to link this library with independent modules to + * produce an executable, regardless of the license terms of these + * independent modules, and to copy and distribute the resulting + * executable under terms of your choice, provided that you also meet, + * for each linked independent module, the terms and conditions of the + * license of that module. An independent module is a module which is + * not derived from or based on this library. If you modify this + * library, you may extend this exception to your version of the + * library, but you are not obliged to do so. If you do not wish to do + * so, delete this exception statement from your version. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +package org.polymc; + +import org.polymc.impl.OneSixLauncher; +import org.polymc.utils.Parameters; + +import java.util.HashMap; +import java.util.Map; + +public final class LauncherFactory { + + private static final LauncherFactory INSTANCE = new LauncherFactory(); + + private final Map<String, LauncherProvider> launcherRegistry = new HashMap<>(); + + private LauncherFactory() { + launcherRegistry.put("onesix", new LauncherProvider() { + @Override + public Launcher provide(Parameters parameters) { + return new OneSixLauncher(parameters); + } + }); + } + + public Launcher createLauncher(Parameters parameters) { + String name = parameters.first("launcher"); + + LauncherProvider launcherProvider = launcherRegistry.get(name); + + if (launcherProvider == null) + throw new IllegalArgumentException("Invalid launcher type: " + name); + + return launcherProvider.provide(parameters); + } + + public static LauncherFactory getInstance() { + return INSTANCE; + } + + public interface LauncherProvider { + + Launcher provide(Parameters parameters); + + } + +} diff --git a/libraries/launcher/org/polymc/applet/LegacyFrame.java b/libraries/launcher/org/polymc/applet/LegacyFrame.java new file mode 100644 index 00000000..2cdd17d7 --- /dev/null +++ b/libraries/launcher/org/polymc/applet/LegacyFrame.java @@ -0,0 +1,162 @@ +/* + * Copyright 2012-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.polymc.applet; + +import net.minecraft.Launcher; + +import javax.imageio.ImageIO; +import java.applet.Applet; +import java.awt.*; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; + +public final class LegacyFrame extends Frame { + + private static final Logger LOGGER = Logger.getLogger("LegacyFrame"); + + private final Launcher appletWrap; + + public LegacyFrame(String title, Applet mcApplet) { + super(title); + + appletWrap = new Launcher(mcApplet); + + mcApplet.setStub(appletWrap); + + try { + setIconImage(ImageIO.read(new File("icon.png"))); + } catch (IOException e) { + LOGGER.log(Level.WARNING, "Unable to read Minecraft icon!", e); + } + + addWindowListener(new ForceExitHandler()); + } + + public void start ( + String user, + String session, + int winSizeW, + int winSizeH, + boolean maximize, + String serverAddress, + String serverPort + ) { + // Implements support for launching in to multiplayer on classic servers using a mpticket + // file generated by an external program and stored in the instance's root folder. + + Path mpticketFile = + Paths.get(System.getProperty("user.dir"), "..", "mpticket"); + + Path mpticketFileCorrupt = + Paths.get(System.getProperty("user.dir"), "..", "mpticket.corrupt"); + + if (Files.exists(mpticketFile)) { + try { + List<String> lines = Files.readAllLines(mpticketFile, StandardCharsets.UTF_8); + + if (lines.size() < 3) { + Files.move( + mpticketFile, + mpticketFileCorrupt, + StandardCopyOption.REPLACE_EXISTING + ); + + LOGGER.warning("Mpticket file is corrupted!"); + } else { + // Assumes parameters are valid and in the correct order + appletWrap.setParameter("server", lines.get(0)); + appletWrap.setParameter("port", lines.get(1)); + appletWrap.setParameter("mppass", lines.get(2)); + } + } catch (IOException e) { + LOGGER.log(Level.WARNING, "Unable to read mpticket file!", e); + } + } + + if (serverAddress != null) { + appletWrap.setParameter("server", serverAddress); + appletWrap.setParameter("port", serverPort); + } + + appletWrap.setParameter("username", user); + appletWrap.setParameter("sessionid", session); + appletWrap.setParameter("stand-alone", "true"); // Show the quit button. + appletWrap.setParameter("haspaid", "true"); // Some old versions need this for world saves to work. + appletWrap.setParameter("demo", "false"); + appletWrap.setParameter("fullscreen", "false"); + + add(appletWrap); + + appletWrap.setPreferredSize(new Dimension(winSizeW, winSizeH)); + + pack(); + + setLocationRelativeTo(null); + setResizable(true); + + if (maximize) + this.setExtendedState(MAXIMIZED_BOTH); + + validate(); + + appletWrap.init(); + appletWrap.start(); + + setVisible(true); + } + + private final class ForceExitHandler extends WindowAdapter { + + @Override + public void windowClosing(WindowEvent e) { + new Thread(new Runnable() { + @Override + public void run() { + try { + Thread.sleep(30000L); + } catch (InterruptedException localInterruptedException) { + localInterruptedException.printStackTrace(); + } + + LOGGER.info("Forcing exit!"); + + System.exit(0); + } + }).start(); + + if (appletWrap != null) { + appletWrap.stop(); + appletWrap.destroy(); + } + + // old minecraft versions can hang without this >_< + System.exit(0); + } + + } + +} diff --git a/libraries/launcher/org/polymc/exception/ParameterNotFoundException.java b/libraries/launcher/org/polymc/exception/ParameterNotFoundException.java new file mode 100644 index 00000000..2044814e --- /dev/null +++ b/libraries/launcher/org/polymc/exception/ParameterNotFoundException.java @@ -0,0 +1,25 @@ +/* + * Copyright 2012-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.polymc.exception; + +public final class ParameterNotFoundException extends IllegalArgumentException { + + public ParameterNotFoundException(String key) { + super("Unknown parameter name: " + key); + } + +} diff --git a/libraries/launcher/org/polymc/exception/ParseException.java b/libraries/launcher/org/polymc/exception/ParseException.java new file mode 100644 index 00000000..2f2f8294 --- /dev/null +++ b/libraries/launcher/org/polymc/exception/ParseException.java @@ -0,0 +1,25 @@ +/* + * Copyright 2012-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.polymc.exception; + +public final class ParseException extends IllegalArgumentException { + + public ParseException(String message) { + super(message); + } + +} diff --git a/libraries/launcher/org/polymc/impl/OneSixLauncher.java b/libraries/launcher/org/polymc/impl/OneSixLauncher.java new file mode 100644 index 00000000..362ff8d6 --- /dev/null +++ b/libraries/launcher/org/polymc/impl/OneSixLauncher.java @@ -0,0 +1,189 @@ +/* Copyright 2012-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.polymc.impl; + +import org.polymc.Launcher; +import org.polymc.applet.LegacyFrame; +import org.polymc.utils.Parameters; +import org.polymc.utils.Utils; + +import java.applet.Applet; +import java.io.File; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.Collections; +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; + +public final class OneSixLauncher implements Launcher { + + private static final int DEFAULT_WINDOW_WIDTH = 854; + private static final int DEFAULT_WINDOW_HEIGHT = 480; + + private static final Logger LOGGER = Logger.getLogger("OneSixLauncher"); + + // parameters, separated from ParamBucket + private final List<String> mcParams; + private final List<String> traits; + private final String appletClass; + private final String mainClass; + private final String userName, sessionId; + private final String windowTitle; + + // secondary parameters + private final int winSizeW; + private final int winSizeH; + private final boolean maximize; + private final String cwd; + + private final String serverAddress; + private final String serverPort; + + private final ClassLoader classLoader; + + public OneSixLauncher(Parameters params) { + classLoader = ClassLoader.getSystemClassLoader(); + + mcParams = params.allSafe("param", Collections.<String>emptyList()); + mainClass = params.firstSafe("mainClass", "net.minecraft.client.Minecraft"); + appletClass = params.firstSafe("appletClass", "net.minecraft.client.MinecraftApplet"); + traits = params.allSafe("traits", Collections.<String>emptyList()); + + userName = params.first("userName"); + sessionId = params.first("sessionId"); + windowTitle = params.firstSafe("windowTitle", "Minecraft"); + + serverAddress = params.firstSafe("serverAddress", null); + serverPort = params.firstSafe("serverPort", null); + + cwd = System.getProperty("user.dir"); + + String windowParams = params.firstSafe("windowParams", null); + + if (windowParams != null) { + String[] dimStrings = windowParams.split("x"); + + if (windowParams.equalsIgnoreCase("max")) { + maximize = true; + + winSizeW = DEFAULT_WINDOW_WIDTH; + winSizeH = DEFAULT_WINDOW_HEIGHT; + } else if (dimStrings.length == 2) { + maximize = false; + + winSizeW = Integer.parseInt(dimStrings[0]); + winSizeH = Integer.parseInt(dimStrings[1]); + } else { + throw new IllegalArgumentException("Unexpected window size parameter value: " + windowParams); + } + } else { + maximize = false; + + winSizeW = DEFAULT_WINDOW_WIDTH; + winSizeH = DEFAULT_WINDOW_HEIGHT; + } + } + + private void invokeMain(Class<?> mainClass) throws Exception { + Method method = mainClass.getMethod("main", String[].class); + + method.invoke(null, (Object) mcParams.toArray(new String[0])); + } + + private void legacyLaunch() throws Exception { + // Get the Minecraft Class and set the base folder + Class<?> minecraftClass = classLoader.loadClass(mainClass); + + Field baseDirField = Utils.getMinecraftBaseDirField(minecraftClass); + + if (baseDirField == null) { + LOGGER.warning("Could not find Minecraft path field."); + } else { + baseDirField.setAccessible(true); + + baseDirField.set(null, new File(cwd)); + } + + System.setProperty("minecraft.applet.TargetDirectory", cwd); + + if (!traits.contains("noapplet")) { + LOGGER.info("Launching with applet wrapper..."); + + try { + Class<?> mcAppletClass = classLoader.loadClass(appletClass); + + Applet mcApplet = (Applet) mcAppletClass.getConstructor().newInstance(); + + LegacyFrame mcWindow = new LegacyFrame(windowTitle, mcApplet); + + mcWindow.start( + userName, + sessionId, + winSizeW, + winSizeH, + maximize, + serverAddress, + serverPort + ); + + return; + } catch (Exception e) { + LOGGER.log(Level.SEVERE, "Applet wrapper failed: ", e); + + LOGGER.warning("Falling back to using main class."); + } + } + + invokeMain(minecraftClass); + } + + private void launchWithMainClass() throws Exception { + // window size, title and state, onesix + + // FIXME: there is no good way to maximize the minecraft window in onesix. + // the following often breaks linux screen setups + // mcparams.add("--fullscreen"); + + if (!maximize) { + mcParams.add("--width"); + mcParams.add(Integer.toString(winSizeW)); + mcParams.add("--height"); + mcParams.add(Integer.toString(winSizeH)); + } + + if (serverAddress != null) { + mcParams.add("--server"); + mcParams.add(serverAddress); + mcParams.add("--port"); + mcParams.add(serverPort); + } + + invokeMain(classLoader.loadClass(mainClass)); + } + + @Override + public void launch() throws Exception { + if (traits.contains("legacyLaunch") || traits.contains("alphaLaunch")) { + // legacy launch uses the applet wrapper + legacyLaunch(); + } else { + // normal launch just calls main() + launchWithMainClass(); + } + } + +} diff --git a/libraries/launcher/org/polymc/utils/Parameters.java b/libraries/launcher/org/polymc/utils/Parameters.java new file mode 100644 index 00000000..864d3cd2 --- /dev/null +++ b/libraries/launcher/org/polymc/utils/Parameters.java @@ -0,0 +1,78 @@ +/* + * Copyright 2012-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.polymc.utils; + +import org.polymc.exception.ParameterNotFoundException; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public final class Parameters { + + private final Map<String, List<String>> paramsMap = new HashMap<>(); + + public void add(String key, String value) { + List<String> params = paramsMap.get(key); + + if (params == null) { + params = new ArrayList<>(); + + paramsMap.put(key, params); + } + + params.add(value); + } + + public List<String> all(String key) throws ParameterNotFoundException { + List<String> params = paramsMap.get(key); + + if (params == null) + throw new ParameterNotFoundException(key); + + return params; + } + + public List<String> allSafe(String key, List<String> def) { + List<String> params = paramsMap.get(key); + + if (params == null || params.isEmpty()) + return def; + + return params; + } + + public String first(String key) throws ParameterNotFoundException { + List<String> list = all(key); + + if (list.isEmpty()) + throw new ParameterNotFoundException(key); + + return list.get(0); + } + + public String firstSafe(String key, String def) { + List<String> params = paramsMap.get(key); + + if (params == null || params.isEmpty()) + return def; + + return params.get(0); + } + +} diff --git a/libraries/launcher/org/polymc/utils/Utils.java b/libraries/launcher/org/polymc/utils/Utils.java new file mode 100644 index 00000000..12d6e1aa --- /dev/null +++ b/libraries/launcher/org/polymc/utils/Utils.java @@ -0,0 +1,49 @@ +/* + * Copyright 2012-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.polymc.utils; + +import java.io.File; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; + +public final class Utils { + + private Utils() {} + + /** + * Finds a field that looks like a Minecraft base folder in a supplied class + * + * @param clazz the class to scan + */ + public static Field getMinecraftBaseDirField(Class<?> clazz) { + for (Field f : clazz.getDeclaredFields()) { + // Has to be File + if (f.getType() != File.class) + continue; + + // And Private Static. + if (!Modifier.isStatic(f.getModifiers()) || !Modifier.isPrivate(f.getModifiers())) + continue; + + return f; + } + + return null; + } + +} + |