Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions megamek/src/megamek/client/ui/clientGUI/GUIPreferences.java
Original file line number Diff line number Diff line change
Expand Up @@ -423,6 +423,7 @@ public class GUIPreferences extends PreferenceStoreProxy {
public static final String BOARD_EDIT_LOAD_SIZE_HEIGHT = "BoardEditLoadSizeHeight";
public static final String BOARD_EDIT_LOAD_SIZE_WIDTH = "BoardEditLoadSizeWidth";
public static final String BOARD_EDIT_RANDOM_DIALOG_START = "BoardEditRandomDialogStart";
public static final String BOARD_SAVE_INCLUDE_LICENSE = "BoardSaveIncludeLicense";
public static final String ALLY_UNIT_COLOR = "AllyUnitColor";
public static final String MY_UNIT_COLOR = "MyUnitColor";
public static final String ENEMY_UNIT_COLOR = "EnemyUnitColor";
Expand Down Expand Up @@ -519,6 +520,7 @@ protected GUIPreferences() {
.getPreferenceStore("GUIPreferences", getClass().getName(), "megamek.client.ui.swing.GUIPreferences");

store.setDefault(BOARD_EDIT_RANDOM_DIALOG_START, false);
store.setDefault(BOARD_SAVE_INCLUDE_LICENSE, true);
setDefault(ADVANCED_NO_SAVE_NAG, false);
store.setDefault(ADVANCED_SAVE_LOBBY_ON_START, false);
store.setDefault(ADVANCED_MOVE_STEP_DELAY, 50);
Expand Down Expand Up @@ -1778,6 +1780,10 @@ public boolean getBoardEdRndStart() {
return store.getBoolean(BOARD_EDIT_RANDOM_DIALOG_START);
}

public boolean getBoardSaveIncludeLicense() {
return store.getBoolean(BOARD_SAVE_INCLUDE_LICENSE);
}

public void setShadowMap(boolean state) {
store.setValue(SHADOW_MAP, state);
}
Expand Down Expand Up @@ -2712,6 +2718,10 @@ public void setBoardEdRndStart(boolean b) {
store.setValue(BOARD_EDIT_RANDOM_DIALOG_START, b);
}

public void setBoardSaveIncludeLicense(boolean includeLicense) {
store.setValue(BOARD_SAVE_INCLUDE_LICENSE, includeLicense);
}

// region Colours
public Color getMyUnitColor() {
return getColor(MY_UNIT_COLOR);
Expand Down
82 changes: 80 additions & 2 deletions megamek/src/megamek/common/board/Board.java
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import megamek.client.ui.clientGUI.GUIPreferences;
import megamek.common.Configuration;
Expand Down Expand Up @@ -108,6 +110,36 @@ public class Board implements Serializable {
private static final int UNDEFINED_MIN_ELEV = 10000;
private static final int UNDEFINED_MAX_ELEV = -10000;

/**
* License header for board files, compatible with Creative Commons BY-NC-SA 4.0. The year is dynamically set to the
* current year when saving.
*/
public static final String LICENSE_HEADER = """
Comment thread
HammerGS marked this conversation as resolved.
# MegaMek Data (C) %s by The MegaMek Team is licensed under CC BY-NC-SA 4.0.
# To view a copy of this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/
#
# NOTICE: The MegaMek organization is a non-profit group of volunteers
# creating free software for the BattleTech community.
#
# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks
# of The Topps Company, Inc. All Rights Reserved.
#
# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of
# InMediaRes Productions, LLC.
#
# MechWarrior Copyright Microsoft Corporation. MegaMek Data was created under
# Microsoft's "Game Content Usage Rules"
# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or
# affiliated with Microsoft.
""";

/** Regex pattern to extract the copyright year(s) from board file headers. */
private static final Pattern COPYRIGHT_YEAR_PATTERN = Pattern.compile(
"#\\s*MegaMek Data \\(C\\)\\s*(\\d{4})(?:-(\\d{4}))?");

/** The original copyright year from the loaded board file, or -1 if none found. */
private int originalCopyrightYear = -1;

// The min and max elevation values for this board.
// set when getMinElevation/getMax is called for the first time.
private int minElevation = UNDEFINED_MIN_ELEV;
Expand Down Expand Up @@ -1002,7 +1034,28 @@ public void load(InputStream is, @Nullable List<String> errors, boolean continue
Hex[] nd = new Hex[0];
int index = 0;
resetStoredElevation();
try (InputStreamReader isr = new InputStreamReader(is);
originalCopyrightYear = -1;

// Read the entire content first to extract copyright year from header
String content;
try {
content = new String(is.readAllBytes(), StandardCharsets.UTF_8);
} catch (IOException e) {
logger.error(e, "Error reading board file content");
return;
}

// Extract original copyright year from header if present
Matcher matcher = COPYRIGHT_YEAR_PATTERN.matcher(content);
if (matcher.find()) {
try {
originalCopyrightYear = Integer.parseInt(matcher.group(1));
} catch (NumberFormatException ignored) {
// Keep default -1
}
}

try (InputStreamReader isr = new InputStreamReader(new ByteArrayInputStream(content.getBytes(StandardCharsets.UTF_8)));
BufferedReader br = new BufferedReader(isr)) {
StreamTokenizer st = new StreamTokenizer(br);
st.eolIsSignificant(true);
Expand Down Expand Up @@ -1179,10 +1232,35 @@ private boolean isValid(Hex[] data, int width, int height, @Nullable List<String
}

/**
* Writes data for the board, as text to the OutputStream
* Writes data for the board, as text to the OutputStream.
* Uses the GUI preference to determine whether to include the license header.
*
* @param os the OutputStream to write to
*/
public void save(OutputStream os) {
boolean includeLicense = GUIPreferences.getInstance().getBoardSaveIncludeLicense();
save(os, includeLicense);
Comment thread
HammerGS marked this conversation as resolved.
}

/**
* Writes data for the board, as text to the OutputStream.
*
* @param os the OutputStream to write to
* @param includeLicense if true, writes the CC BY-NC-SA 4.0 license header at the start of the file
*/
public void save(OutputStream os, boolean includeLicense) {
try (Writer w = new OutputStreamWriter(os)) {
if (includeLicense) {
int currentYear = Calendar.getInstance().get(Calendar.YEAR);
String yearString;
if ((originalCopyrightYear > 0) && (originalCopyrightYear < currentYear)) {
yearString = originalCopyrightYear + "-" + currentYear;
} else {
yearString = String.valueOf(currentYear);
}
w.write(LICENSE_HEADER.formatted(yearString));
w.write("\r\n");
}
w.write("size " + width + ' ' + height + "\r\n");
if (!roadsAutoExit) {
w.write("option exit_roads_to_pavement false\r\n");
Expand Down
89 changes: 79 additions & 10 deletions megamek/src/megamek/utilities/BoardsTagger.java
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Consumer;
import java.util.stream.Collectors;

import megamek.common.Configuration;
Expand Down Expand Up @@ -380,28 +381,35 @@ public static void runBoardTagger() {
}

/**
* Recursively scans the supplied file/directory for any boards and auto-tags them.
* Recursively scans the supplied file/directory and applies the given processor to each file found.
*
* @param file the file or directory to scan
* @param fileProcessor the action to perform on each file
*/
private static void scanForBoards(File file, Map<String, List<String>> boardCheckSum) {
private static void scanBoardFiles(File file, Consumer<File> fileProcessor) {
if (file.isDirectory()) {
String[] fileList = file.list();
if (fileList != null) {
for (String filename : fileList) {
File filepath = new File(file, filename);
if (filepath.isDirectory()) {
scanForBoards(new File(file, filename), boardCheckSum);
} else {
tagBoard(filepath);
checkSum(boardCheckSum, filepath);
}
scanBoardFiles(filepath, fileProcessor);
}
}
} else {
tagBoard(file);
checkSum(boardCheckSum, file);
fileProcessor.accept(file);
}
}

/**
* Recursively scans the supplied file/directory for any boards and auto-tags them.
*/
private static void scanForBoards(File file, Map<String, List<String>> boardCheckSum) {
scanBoardFiles(file, boardFile -> {
tagBoard(boardFile);
checkSum(boardCheckSum, boardFile);
});
}

/**
* Scans the board for the types and number of terrains present, deletes old automatic tags and applies new
* automatic tags as appropriate.
Expand Down Expand Up @@ -494,4 +502,65 @@ private static void checkSum(Map<String, List<String>> boardCheckSum, File board
logger.error(e, "SHA-256 Algorithm Can't be Found");
}
}

/**
* Runs the copyright header updater on all boards in the default boards directory. Adds or updates the MegaMek Data
* copyright header to all valid board files.
*/
public static void runCopyrightHeaderUpdater() {
Comment thread
HammerGS marked this conversation as resolved.
try {
File boardDir = Configuration.boardsDir();
scanForBoardsAndUpdateHeaders(boardDir);
} catch (Exception ex) {
logger.error(ex, "Copyright header updater cannot scan boards");
}

logger.info("Copyright header update finished.");
}
Comment thread
HammerGS marked this conversation as resolved.

/**
* Recursively scans the supplied file/directory for any boards and updates their copyright headers.
*
* @param file the file or directory to scan
*/
private static void scanForBoardsAndUpdateHeaders(File file) {
scanBoardFiles(file, BoardsTagger::updateCopyrightHeader);
}

/**
* Updates the copyright header on a single board file. If the board already has a copyright header (lines starting
* with #), it is replaced. If not, a new header is added at the beginning of the file.
*
* @param boardFile the board file to update
*/
private static void updateCopyrightHeader(File boardFile) {
// If this isn't a board, ignore it
if (!boardFile.toString().endsWith(".board")) {
return;
}

// Load the board
Board board = new Board();
try (InputStream is = new FileInputStream(boardFile)) {
List<String> errors = new ArrayList<>();
board.load(is, errors, true);
if (!errors.isEmpty()) {
logger.debug("Board has errors, skipping: {}", boardFile);
return;
}
} catch (Exception e) {
logger.error(e, "Could not load board: {}", boardFile);
Comment thread Dismissed
return;
}

// Re-save the board with the license header
try (OutputStream os = new FileOutputStream(boardFile)) {
board.save(os, true);
if (DEBUG) {
logger.debug("Updated copyright header: {}", boardFile);
}
} catch (Exception ex) {
logger.error(ex, "Could not save board: {}", boardFile);
Comment thread Dismissed
}
}
}
Loading