485 lines
25 KiB
HTML
485 lines
25 KiB
HTML
|
||
|
||
|
||
<!DOCTYPE html>
|
||
<html id="htmlId">
|
||
<head>
|
||
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
|
||
<title>Coverage Report > ClientLauncherTUI</title>
|
||
<style type="text/css">
|
||
@import "../../css/coverage.css";
|
||
@import "../../css/idea.min.css";
|
||
</style>
|
||
<script type="text/javascript" src="../../js/highlight.min.js"></script>
|
||
<script type="text/javascript" src="../../js/highlightjs-line-numbers.min.js"></script>
|
||
</head>
|
||
|
||
<body>
|
||
<div class="content">
|
||
<div class="breadCrumbs">
|
||
Current scope: <a href="../../index.html">all classes</a>
|
||
<span class="separator">|</span>
|
||
<a href="../index.html">it.polimi.ingsw.gc14</a>
|
||
</div>
|
||
|
||
<h1>Coverage Summary for Class: ClientLauncherTUI (it.polimi.ingsw.gc14)</h1>
|
||
|
||
<table class="coverageStats">
|
||
<tr>
|
||
<th class="name">Class</th>
|
||
<th class="coverageStat
|
||
">
|
||
Class, %
|
||
</th>
|
||
<th class="coverageStat
|
||
">
|
||
Method, %
|
||
</th>
|
||
<th class="coverageStat
|
||
">
|
||
Branch, %
|
||
</th>
|
||
<th class="coverageStat
|
||
">
|
||
Line, %
|
||
</th>
|
||
</tr>
|
||
<tr>
|
||
<td class="name">ClientLauncherTUI</td>
|
||
<td class="coverageStat">
|
||
<span class="percent">
|
||
0%
|
||
</span>
|
||
<span class="absValue">
|
||
(0/1)
|
||
</span>
|
||
</td>
|
||
<td class="coverageStat">
|
||
<span class="percent">
|
||
0%
|
||
</span>
|
||
<span class="absValue">
|
||
(0/12)
|
||
</span>
|
||
</td>
|
||
<td class="coverageStat">
|
||
<span class="percent">
|
||
0%
|
||
</span>
|
||
<span class="absValue">
|
||
(0/62)
|
||
</span>
|
||
</td>
|
||
<td class="coverageStat">
|
||
<span class="percent">
|
||
0%
|
||
</span>
|
||
<span class="absValue">
|
||
(0/139)
|
||
</span>
|
||
</td>
|
||
</tr>
|
||
|
||
</table>
|
||
|
||
<br/>
|
||
<br/>
|
||
|
||
|
||
<pre>
|
||
<code class="sourceCode" id="sourceCode"> package it.polimi.ingsw.gc14;
|
||
|
||
import it.polimi.ingsw.gc14.Controller.ClientController;
|
||
import it.polimi.ingsw.gc14.Model.GamePackage.GameStages;
|
||
import it.polimi.ingsw.gc14.Network.IClient;
|
||
import it.polimi.ingsw.gc14.Network.NetworkConfig;
|
||
import it.polimi.ingsw.gc14.Network.InterfaceResolver;
|
||
import it.polimi.ingsw.gc14.Network.RMI.Client.RMIClient;
|
||
import it.polimi.ingsw.gc14.Network.TCP.Client.TCPClient;
|
||
import it.polimi.ingsw.gc14.View.TUI.TUI;
|
||
|
||
import org.jline.builtins.Completers.TreeCompleter;
|
||
import org.jline.reader.*;
|
||
import org.jline.reader.impl.completer.StringsCompleter;
|
||
import org.jline.terminal.Terminal;
|
||
import org.jline.terminal.TerminalBuilder;
|
||
|
||
import java.io.IOException;
|
||
import java.io.PrintStream;
|
||
import java.nio.charset.StandardCharsets;
|
||
|
||
import static org.jline.builtins.Completers.TreeCompleter.node;
|
||
|
||
/**
|
||
* Entry point for the TUI-based game client.
|
||
* Handles login, connects to the server, and drives the JLine-powered command loop.
|
||
*/
|
||
<b class="nc"> public class ClientLauncherTUI {</b>
|
||
/** TUI view shared with the client controller. */
|
||
private TUI view;
|
||
/** JLine terminal used for all input/output. */
|
||
private Terminal terminal;
|
||
/** Line reader used during active gameplay, with tab-completion wired. */
|
||
private LineReader gameReader;
|
||
/** Username of the currently logged-in player; empty string before login. */
|
||
<b class="nc"> private String currentUsername = "";</b>
|
||
|
||
private static final String BUILDING_DETAILS =
|
||
"ID 0 — Each time you complete a set of 6 different Character cards, take 5 \uD83C\uDF56. (Sets completed before acquiring don't count.)\n" +
|
||
"ID 1 — During the Sustenance Event, pay 1 less \uD83C\uDF56 for each Artist/Inventor/Gatherer in your tribe.\n" +
|
||
"ID 2 — During the Shamanic Ritual Event, you do not lose Prestige Points if you have fewer ⭐ than all other players.\n" +
|
||
"ID 3 — When you move your Totem to a Food-bonus space, take 1 extra \uD83C\uDF56. If placed last, pay 1 \uD83C\uDF56 normally (no effect).\n" +
|
||
"ID 4 — Each time you obtain a pair of identical Inventors (same icon), take 3 \uD83C\uDF56. (Pairs owned before don't count.)\n" +
|
||
"ID 5 — During the Shamanic Ritual Event, your tribe has 3 additional ⭐ icons.\n" +
|
||
"ID 6 — During the Shamanic Ritual Event, if you have more ⭐ than any other player, gain double Prestige. Ties still count.\n" +
|
||
"ID 7 — During the Hunt Event, take 1 \uD83C\uDF56 and gain 1 extra Prestige Point for each Hunter in your tribe.\n" +
|
||
"ID 8 — At end of game, gain double the Prestige Points shown on Builder cards in your tribe.\n" +
|
||
"ID 9 — During the Cave Paintings Event, take 1 \uD83C\uDF56 for each Artist in your tribe.\n" +
|
||
"ID 10 — At end of game, gain 6 Prestige Points for each complete set of 6 different Character cards.\n" +
|
||
"ID 11 — At end of game, gain the indicated Prestige Points for each Character card of the indicated type.\n" +
|
||
"ID 12 — After all Totems return to Turn Order (before End of Round), take 1 Character or 1 Building card from the top row (paying its cost).\n" +
|
||
"ID 13 — At end of game, gain 25 Prestige Points.";
|
||
|
||
private static final String EVENT_DETAILS =
|
||
"SUSTENANCE — Pay 1 \uD83C\uDF56 per Character (Buildings don't count). If you can't feed all Characters, lose the\n" +
|
||
" Prestige Points shown on the card for each one you couldn't feed. Each Gatherer gives a 3\uD83C\uDF56 discount.\n" +
|
||
" You cannot choose to lose PP to avoid paying Food. Sustenance resolves last if multiple Events occur.\n" +
|
||
"\n" +
|
||
"HUNT — Take 1 \uD83C\uDF56 and gain the Prestige Points shown on the card for each Hunter in your tribe.\n" +
|
||
"\n" +
|
||
"SHAMANIC RITUAL — Player with the most \uD83C\uDF1F icons gains the Prestige Points on the card.\n" +
|
||
" Player with the fewest \uD83C\uDF1F icons loses those Prestige Points. In case of a tie, all tied players gain/lose.\n" +
|
||
"\n" +
|
||
"CAVE PAINTINGS — If you have fewer Artists than the top-line threshold, lose the indicated Prestige Points.\n" +
|
||
" If you meet the bottom-line threshold, gain the indicated Prestige Points for each Artist in your tribe.";
|
||
|
||
private static final String CHARACTER_DETAILS =
|
||
"INVENTORS — At end of game, gain PP equal to (number of Inventors) × (number of different Invention icons).\n" +
|
||
" There are 10 different Invention icons.\n" +
|
||
"\n" +
|
||
"GATHERERS — During Sustenance, each Gatherer gives a 3\uD83C\uDF56 discount on what you owe.\n" +
|
||
" Note: you do not take any Food from Gatherers under any circumstances.\n" +
|
||
"\n" +
|
||
"SHAMANS — Each Shaman shows 1–3 \uD83C\uDF1F icons. During Shamanic Ritual, majority = gain PP; minority = lose PP.\n" +
|
||
"\n" +
|
||
"BUILDERS — During the game, each Builder reduces the \uD83C\uDF56 cost of every Building card by the amount\n" +
|
||
" shown in the top-right corner. At end of game, each Builder gives the PP shown in the bottom-left corner.\n" +
|
||
"\n" +
|
||
"ARTISTS — During Cave Paintings, gain or lose PP based on Artist count (see 'details events').\n" +
|
||
" At end of game, gain 10 PP for every 2 Artists in your tribe.\n" +
|
||
"\n" +
|
||
"HUNTERS — Adding a Hunter without the \uD83C\uDF56 icon: nothing. Adding a Hunter WITH the \uD83C\uDF56 icon:\n" +
|
||
" immediately take 1 \uD83C\uDF56 for each Hunter in your tribe (with or without the icon).\n" +
|
||
" During Hunt Event, take \uD83C\uDF56 and gain PP based on your Hunter count (see 'details events').";
|
||
/**
|
||
* Launches the TUI client.
|
||
*
|
||
* @param args command-line arguments (unused).
|
||
* @throws InterruptedException if the thread is interrupted during startup.
|
||
*/
|
||
static void main(String[] args) throws InterruptedException {
|
||
<b class="nc"> new ClientLauncherTUI().start();</b>
|
||
}
|
||
|
||
/**
|
||
* Starts the TUI client. Creates a JLine terminal, loops through login → game → rematch.
|
||
*
|
||
*/
|
||
private void start() {
|
||
try {
|
||
<b class="nc"> System.setOut(new PrintStream(System.out, true, StandardCharsets.UTF_8));</b>
|
||
<b class="nc"> System.setErr(new PrintStream(System.err, true, StandardCharsets.UTF_8));</b>
|
||
<b class="nc"> terminal = TerminalBuilder.builder()</b>
|
||
<b class="nc"> .system(true)</b>
|
||
<b class="nc"> .encoding(StandardCharsets.UTF_8)</b>
|
||
<b class="nc"> .build();</b>
|
||
} catch (IOException e) {
|
||
<b class="nc"> System.err.println("Could not initialise terminal: " + e.getMessage());</b>
|
||
return;
|
||
}
|
||
<b class="nc"> terminal.writer().print("\033[H\033[2J");</b>
|
||
<b class="nc"> terminal.writer().flush();</b>
|
||
|
||
<b class="nc"> view = new TUI(null);</b>
|
||
<b class="nc"> ClientController controller = new ClientController(view);</b>
|
||
<b class="nc"> gameReader = buildGameReader();</b>
|
||
<b class="nc"> view.setLineReader(gameReader);</b>
|
||
|
||
while (true) {
|
||
<b class="nc"> if (!doLogin(controller)) continue;</b>
|
||
|
||
<b class="nc"> while (controller.getClient() != null) {</b>
|
||
<b class="nc"> String prompt = buildPrompt();</b>
|
||
String line;
|
||
try {
|
||
<b class="nc"> line = gameReader.readLine(prompt);</b>
|
||
} catch (UserInterruptException e) {
|
||
<b class="nc"> safeQuit(controller);</b>
|
||
return;
|
||
} catch (EndOfFileException e) {
|
||
<b class="nc"> safeQuit(controller);</b>
|
||
return;
|
||
}
|
||
<b class="nc"> if (line == null || line.isBlank()) continue;</b>
|
||
<b class="nc"> boolean rematch = handleCommand(line.trim(), controller);</b>
|
||
<b class="nc"> if (rematch) break;</b>
|
||
}
|
||
}
|
||
}
|
||
/**
|
||
* Prompts for username, player count, network type, and server IP, then connects.
|
||
*
|
||
* @param controller the client controller to connect.
|
||
* @return {@code true} on successful connection; {@code false} if input is invalid or connection fails.
|
||
*/
|
||
private boolean doLogin(ClientController controller) {
|
||
<b class="nc"> LineReader loginReader = LineReaderBuilder.builder()</b>
|
||
<b class="nc"> .terminal(terminal)</b>
|
||
<b class="nc"> .completer(new StringsCompleter("rmi", "tcp"))</b>
|
||
<b class="nc"> .build();</b>
|
||
<b class="nc"> LineReader ipReader = LineReaderBuilder.builder()</b>
|
||
<b class="nc"> .terminal(terminal)</b>
|
||
<b class="nc"> .completer(new StringsCompleter("localhost"))</b>
|
||
<b class="nc"> .build();</b>
|
||
|
||
<b class="nc"> view.setLineReader(loginReader);</b>
|
||
try {
|
||
<b class="nc"> return doLoginInner(controller, loginReader, ipReader);</b>
|
||
} finally {
|
||
<b class="nc"> view.setLineReader(gameReader);</b>
|
||
}
|
||
}
|
||
/**
|
||
* Reads login fields from the terminal and connects to the server.
|
||
*
|
||
* @param controller the client controller to connect.
|
||
* @param loginReader line reader used for username, player count, and network type.
|
||
* @param ipReader line reader with localhost auto-complete for the server IP.
|
||
* @return {@code true} on successful connection; {@code false} if input is invalid or connection fails.
|
||
*/
|
||
private boolean doLoginInner(ClientController controller, LineReader loginReader, LineReader ipReader) {
|
||
String username, networkStr, ip;
|
||
int nPlayers;
|
||
|
||
try {
|
||
<b class="nc"> username = loginReader.readLine("Username: ").trim();</b>
|
||
<b class="nc"> nPlayers = Integer.parseInt(loginReader.readLine("Number of players [2-5]: ").trim());</b>
|
||
<b class="nc"> networkStr = loginReader.readLine("Network [rmi/tcp]: ").trim().toLowerCase();</b>
|
||
<b class="nc"> ip = ipReader.readLine("Server IP [localhost]: ").trim();</b>
|
||
<b class="nc"> if (ip.isEmpty()) ip = "localhost";</b>
|
||
} catch (UserInterruptException | EndOfFileException e) {
|
||
<b class="nc"> System.exit(0);</b>
|
||
<b class="nc"> return false;</b>
|
||
} catch (NumberFormatException e) {
|
||
<b class="nc"> loginReader.printAbove("\033[H\033[2J" + "\033[31mERROR: Invalid number of players.\033[0m");</b>
|
||
<b class="nc"> return false;</b>
|
||
}
|
||
|
||
<b class="nc"> currentUsername = username;</b>
|
||
<b class="nc"> view.setUsername(username);</b>
|
||
<b class="nc"> controller.setMyUsername(username);</b>
|
||
|
||
IClient client;
|
||
<b class="nc"> if (networkStr.equals("rmi")) {</b>
|
||
String myIP;
|
||
try {
|
||
<b class="nc"> myIP = InterfaceResolver.resolveLocalInterface(ip);</b>
|
||
} catch (Exception e) {
|
||
<b class="nc"> loginReader.printAbove("\033[H\033[2J" + "\033[31mERROR: Cannot resolve local interface: " + e.getMessage() + "\033[0m");</b>
|
||
<b class="nc"> return false;</b>
|
||
}
|
||
<b class="nc"> System.setProperty("java.rmi.server.hostname", myIP);</b>
|
||
<b class="nc"> client = new RMIClient(controller, ip, NetworkConfig.RMI_PORT, myIP);</b>
|
||
<b class="nc"> ErrorType err = client.connect(username, nPlayers);</b>
|
||
<b class="nc"> if (err != null) {</b>
|
||
<b class="nc"> loginReader.printAbove("\033[H\033[2J" + "\033[31mERROR: " + err + "\033[0m");</b>
|
||
<b class="nc"> return false;</b>
|
||
}
|
||
<b class="nc"> System.out.println("Connected via RMI, waiting for other players...");</b>
|
||
<b class="nc"> controller.setClient(client);</b>
|
||
|
||
<b class="nc"> } else if (networkStr.equals("tcp")) {</b>
|
||
<b class="nc"> client = new TCPClient(controller, ip, NetworkConfig.TCP_PORT, NetworkConfig.HEARTBEAT_PORT);</b>
|
||
<b class="nc"> ErrorType err = client.connect(username, nPlayers);</b>
|
||
<b class="nc"> if (err != null) {</b>
|
||
<b class="nc"> loginReader.printAbove("\033[H\033[2J" + "\033[31mERROR: " + err + "\033[0m");</b>
|
||
<b class="nc"> return false;</b>
|
||
}
|
||
<b class="nc"> System.out.println("Connected via TCP, waiting for other players...");</b>
|
||
<b class="nc"> controller.setClient(client);</b>
|
||
|
||
} else {
|
||
<b class="nc"> loginReader.printAbove("\033[H\033[2J" + "\033[31mERROR: Unknown network type '" + networkStr + "'. Use rmi or tcp.\033[0m");</b>
|
||
<b class="nc"> return false;</b>
|
||
}
|
||
<b class="nc"> return true;</b>
|
||
}
|
||
/**
|
||
* Builds the JLine {@link LineReader} used during gameplay, with tab-completion
|
||
* for all valid commands.
|
||
*
|
||
* @return the configured {@link LineReader}.
|
||
*/
|
||
private LineReader buildGameReader() {
|
||
<b class="nc"> Completer completer = new TreeCompleter(</b>
|
||
<b class="nc"> node("slot"),</b>
|
||
<b class="nc"> node("draw",</b>
|
||
<b class="nc"> node("upper",</b>
|
||
<b class="nc"> node("tribe"),</b>
|
||
<b class="nc"> node("building")),</b>
|
||
<b class="nc"> node("lower",</b>
|
||
<b class="nc"> node("tribe"),</b>
|
||
<b class="nc"> node("building"))),</b>
|
||
<b class="nc"> node("totem"),</b>
|
||
<b class="nc"> node("skip"),</b>
|
||
<b class="nc"> node("clear"),</b>
|
||
<b class="nc"> node("details", node("buildings"), node("events"), node("characters")),</b>
|
||
<b class="nc"> node("rematch"),</b>
|
||
<b class="nc"> node("quit")</b>
|
||
);
|
||
<b class="nc"> return LineReaderBuilder.builder()</b>
|
||
<b class="nc"> .terminal(terminal)</b>
|
||
<b class="nc"> .completer(completer)</b>
|
||
<b class="nc"> .option(LineReader.Option.DISABLE_EVENT_EXPANSION, true)</b>
|
||
<b class="nc"> .build();</b>
|
||
}
|
||
/**
|
||
* Returns the command-line prompt string showing the current username.
|
||
*
|
||
* @return the prompt string.
|
||
*/
|
||
private String buildPrompt() {
|
||
<b class="nc"> return currentUsername.isEmpty() ? "> " : currentUsername + "> ";</b>
|
||
}
|
||
|
||
/** Returns {@code true} when the caller should break out of the game loop (rematch requested). */
|
||
private boolean handleCommand(String line, ClientController controller) {
|
||
<b class="nc"> String[] parts = line.split("\\s+");</b>
|
||
<b class="nc"> String cmd = parts[0].toLowerCase();</b>
|
||
|
||
<b class="nc"> switch (cmd) {</b>
|
||
case "slot" -> {
|
||
<b class="nc"> int pos = parsePos(parts, 1);</b>
|
||
<b class="nc"> controller.slotChoice(pos);</b>
|
||
}
|
||
<b class="nc"> case "draw" -> handleDraw(parts, controller);</b>
|
||
case "totem" -> {
|
||
<b class="nc"> int pos = parsePos(parts, 1);</b>
|
||
<b class="nc"> controller.totemChoice(pos);</b>
|
||
}
|
||
<b class="nc"> case "skip" -> controller.skipTurn();</b>
|
||
<b class="nc"> case "clear" -> handleRender();</b>
|
||
case "details" -> {
|
||
<b class="nc"> if (parts.length > 1 && parts[1].equalsIgnoreCase("buildings"))</b>
|
||
<b class="nc"> gameReader.printAbove(BUILDING_DETAILS);</b>
|
||
<b class="nc"> else if (parts.length > 1 && parts[1].equalsIgnoreCase("events"))</b>
|
||
<b class="nc"> gameReader.printAbove(EVENT_DETAILS);</b>
|
||
<b class="nc"> else if (parts.length > 1 && parts[1].equalsIgnoreCase("characters"))</b>
|
||
<b class="nc"> gameReader.printAbove(CHARACTER_DETAILS);</b>
|
||
else
|
||
<b class="nc"> gameReader.printAbove("Usage: details buildings | details events | details characters");</b>
|
||
}
|
||
case "rematch" -> {
|
||
<b class="nc"> if (controller.getMiniModel() == null</b>
|
||
<b class="nc"> || !controller.getMiniModel().currentState.getGameStage().equals(GameStages.ENDED)) {</b>
|
||
<b class="nc"> System.out.println("Game not ended yet. Use 'quit' to disconnect.");</b>
|
||
<b class="nc"> return false;</b>
|
||
}
|
||
<b class="nc"> controller.disconnect();</b>
|
||
<b class="nc"> controller.setClient(null);</b>
|
||
<b class="nc"> controller.setModel(null);</b>
|
||
<b class="nc"> return true;</b>
|
||
}
|
||
case "quit" -> {
|
||
<b class="nc"> safeQuit(controller);</b>
|
||
<b class="nc"> System.exit(0);</b>
|
||
}
|
||
<b class="nc"> default -> view.showError(ErrorType.GENERIC_ERROR, "Unknown command: " + cmd);</b>
|
||
}
|
||
<b class="nc"> return false;</b>
|
||
}
|
||
/**
|
||
* Handles the {@code draw} command, dispatching to the appropriate controller draw method.
|
||
*
|
||
* @param parts the tokenized command: {@code [draw, upper|lower, tribe|building, pos]}.
|
||
* @param controller the client controller to invoke.
|
||
*/
|
||
private void handleDraw(String[] parts, ClientController controller) {
|
||
<b class="nc"> if (parts.length < 4) {</b>
|
||
<b class="nc"> view.showError(ErrorType.GENERIC_ERROR, "Usage: draw upper|lower tribe|building <pos>");</b>
|
||
return;
|
||
}
|
||
<b class="nc"> boolean upper = parts[1].equalsIgnoreCase("upper");</b>
|
||
<b class="nc"> boolean tribe = parts[2].equalsIgnoreCase("tribe");</b>
|
||
<b class="nc"> int pos = parsePos(parts, 3);</b>
|
||
<b class="nc"> if (pos < 0) return;</b>
|
||
|
||
<b class="nc"> if (upper && tribe) controller.drawUpperTribeCard(pos);</b>
|
||
<b class="nc"> else if (upper) controller.drawUpperBuildingCard(pos);</b>
|
||
<b class="nc"> else if (tribe) controller.drawLowerTribeCard(pos);</b>
|
||
<b class="nc"> else controller.drawLowerBuildingCard(pos);</b>
|
||
}
|
||
/**
|
||
* Handles the {@code clear} command by triggering a full board re-render.
|
||
*
|
||
*/
|
||
private void handleRender() {
|
||
<b class="nc"> view.renderBoard();</b>
|
||
}
|
||
/**
|
||
* Parses an integer position argument at {@code idx} in {@code parts}.
|
||
*
|
||
* @param parts the tokenized command array.
|
||
* @param idx index of the position argument.
|
||
* @return the parsed integer, or {@code -1} if missing or malformed.
|
||
*/
|
||
private int parsePos(String[] parts, int idx) {
|
||
<b class="nc"> if (parts.length <= idx) {</b>
|
||
<b class="nc"> view.showError(ErrorType.GENERIC_ERROR, "Missing position argument.");</b>
|
||
<b class="nc"> return -1;</b>
|
||
}
|
||
try {
|
||
<b class="nc"> return Integer.parseInt(parts[idx]);</b>
|
||
} catch (NumberFormatException e) {
|
||
<b class="nc"> view.showError(ErrorType.GENERIC_ERROR,</b>
|
||
"Invalid position '" + parts[idx] + "' — expected integer.");
|
||
<b class="nc"> return -1;</b>
|
||
}
|
||
}
|
||
/**
|
||
* Disconnects the client if one is currently connected; safe to call when already disconnected.
|
||
*
|
||
* @param controller the client controller to disconnect.
|
||
*/
|
||
private void safeQuit(ClientController controller) {
|
||
<b class="nc"> if (controller.getClient() != null) controller.disconnect();</b>
|
||
}
|
||
}
|
||
</code>
|
||
</pre>
|
||
</div>
|
||
|
||
<script type="text/javascript">
|
||
(function() {
|
||
var msie = false, msie9 = false;
|
||
/*@cc_on
|
||
msie = true;
|
||
@if (@_jscript_version >= 9)
|
||
msie9 = true;
|
||
@end
|
||
@*/
|
||
|
||
if (!msie || msie && msie9) {
|
||
hljs.highlightAll()
|
||
hljs.initLineNumbersOnLoad();
|
||
}
|
||
})();
|
||
</script>
|
||
|
||
<div class="footer">
|
||
|
||
<div style="float:right;">generated on 2026-06-19 22:53</div>
|
||
</div>
|
||
</body>
|
||
</html>
|