初版,1.21.4
This commit is contained in:
parent
6e6a2c9333
commit
8fdc1200b2
64
src/main/java/com/example/playertime/PlayerTimeMod.java
Normal file
64
src/main/java/com/example/playertime/PlayerTimeMod.java
Normal file
@ -0,0 +1,64 @@
|
||||
package com.example.playertime;
|
||||
|
||||
import net.fabricmc.api.ModInitializer;
|
||||
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents;
|
||||
import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class PlayerTimeMod implements ModInitializer {
|
||||
public static final Logger LOGGER = LoggerFactory.getLogger("PlayerTimeTracker");
|
||||
private static PlayerTimeTracker timeTracker;
|
||||
private static WebServer webServer;
|
||||
|
||||
@Override
|
||||
public void onInitialize() {
|
||||
try {
|
||||
LOGGER.info("[在线时间] 初始化玩家在线时长视奸MOD");
|
||||
|
||||
ServerLifecycleEvents.SERVER_STARTING.register(server -> {
|
||||
LOGGER.info("[在线时间] 服务器启动 - 初始化跟踪器");
|
||||
timeTracker = new PlayerTimeTracker(server);
|
||||
|
||||
try {
|
||||
webServer = new WebServer(timeTracker, 60048);
|
||||
webServer.start();
|
||||
LOGGER.info("[在线时间] 在线时长Web服务器启动在端口60048");
|
||||
} catch (Exception e) {
|
||||
LOGGER.error("[在线时间] 无法启动 Web 服务器", e);
|
||||
throw new RuntimeException("[在线时间] Web 服务器启动失败", e);
|
||||
}
|
||||
});
|
||||
|
||||
ServerPlayConnectionEvents.JOIN.register((handler, sender, server) -> {
|
||||
if (timeTracker != null) {
|
||||
timeTracker.onPlayerJoin(handler.player);
|
||||
}
|
||||
});
|
||||
|
||||
ServerPlayConnectionEvents.DISCONNECT.register((handler, server) -> {
|
||||
if (timeTracker != null) {
|
||||
timeTracker.onPlayerLeave(handler.player);
|
||||
}
|
||||
});
|
||||
|
||||
ServerLifecycleEvents.SERVER_STOPPING.register(server -> {
|
||||
LOGGER.info("[在线时间] 服务器停止 - 保存数据");
|
||||
if (webServer != null) {
|
||||
webServer.stop();
|
||||
}
|
||||
if (timeTracker != null) {
|
||||
timeTracker.saveAll();
|
||||
}
|
||||
});
|
||||
|
||||
} catch (Exception e) {
|
||||
LOGGER.error("[在线时间] Mod 初始化失败!", e);
|
||||
throw new RuntimeException("[在线时间] Mod 初始化失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
public static PlayerTimeTracker getTimeTracker() {
|
||||
return timeTracker;
|
||||
}
|
||||
}
|
236
src/main/java/com/example/playertime/PlayerTimeTracker.java
Normal file
236
src/main/java/com/example/playertime/PlayerTimeTracker.java
Normal file
@ -0,0 +1,236 @@
|
||||
package com.example.playertime;
|
||||
|
||||
import com.google.gson.*;
|
||||
import com.mojang.authlib.GameProfile;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.network.ServerPlayerEntity;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.time.Instant;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
public class PlayerTimeTracker {
|
||||
private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();
|
||||
private final MinecraftServer server;
|
||||
private final Path dataFile;
|
||||
private final ExecutorService executor = Executors.newSingleThreadExecutor();
|
||||
private final Map<UUID, PlayerTimeData> playerData = new ConcurrentHashMap<>();
|
||||
|
||||
public PlayerTimeTracker(MinecraftServer server) {
|
||||
this.server = server;
|
||||
this.dataFile = server.getRunDirectory().resolve("player_time_data.json");
|
||||
loadData();
|
||||
}
|
||||
|
||||
private boolean isWhitelisted(String playerName) {
|
||||
MinecraftServer server = this.server;
|
||||
if (server == null) return false;
|
||||
|
||||
return server.getPlayerManager().getWhitelist()
|
||||
.isAllowed(server.getUserCache().findByName(playerName).orElse(null));
|
||||
}
|
||||
|
||||
|
||||
public void onPlayerJoin(ServerPlayerEntity player) {
|
||||
PlayerTimeData data = playerData.computeIfAbsent(player.getUuid(), uuid -> new PlayerTimeData());
|
||||
data.lastLogin = Instant.now().getEpochSecond();
|
||||
saveAsync(player.getUuid());
|
||||
}
|
||||
|
||||
public void onPlayerLeave(ServerPlayerEntity player) {
|
||||
PlayerTimeData data = playerData.get(player.getUuid());
|
||||
if (data != null) {
|
||||
long now = Instant.now().getEpochSecond();
|
||||
long sessionTime = now - data.lastLogin;
|
||||
data.totalTime += sessionTime;
|
||||
|
||||
// 维护30天滚动窗口
|
||||
data.rolling30Days.addPlayTime(now, sessionTime);
|
||||
data.rolling7Days.addPlayTime(now, sessionTime);
|
||||
|
||||
data.lastLogin = 0;
|
||||
saveAsync(player.getUuid());
|
||||
}
|
||||
}
|
||||
|
||||
public PlayerTimeStats getPlayerStats(UUID uuid) {
|
||||
PlayerTimeData data = playerData.get(uuid);
|
||||
if (data == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
long now = Instant.now().getEpochSecond();
|
||||
PlayerTimeStats stats = new PlayerTimeStats();
|
||||
stats.totalTime = data.totalTime;
|
||||
|
||||
// 如果玩家在线,添加当前会话时间
|
||||
if (data.lastLogin > 0) {
|
||||
stats.totalTime += (now - data.lastLogin);
|
||||
}
|
||||
|
||||
stats.last30Days = data.rolling30Days.getTotalTime(now);
|
||||
stats.last7Days = data.rolling7Days.getTotalTime(now);
|
||||
|
||||
return stats;
|
||||
}
|
||||
|
||||
public Map<String, String> getWhitelistedPlayerStats() {
|
||||
Map<String, String> stats = new LinkedHashMap<>(); // 保持插入顺序
|
||||
long now = Instant.now().getEpochSecond();
|
||||
|
||||
// 获取白名单玩家名称列表
|
||||
List<String> whitelistedNames = List.of(server.getPlayerManager()
|
||||
.getWhitelist()
|
||||
.getNames());
|
||||
|
||||
playerData.forEach((uuid, data) -> {
|
||||
// 获取玩家名称
|
||||
String playerName = getPlayerName(uuid);
|
||||
|
||||
// 只处理白名单玩家
|
||||
if (whitelistedNames.contains(playerName)) {
|
||||
long totalTime = data.totalTime;
|
||||
if (data.lastLogin > 0) {
|
||||
totalTime += (now - data.lastLogin);
|
||||
}
|
||||
|
||||
stats.put(playerName, "总时长: " + formatTime(totalTime) +
|
||||
" | 30天: " + formatTime(data.rolling30Days.getTotalTime(now)) +
|
||||
" | 7天: " + formatTime(data.rolling7Days.getTotalTime(now)));
|
||||
}
|
||||
});
|
||||
|
||||
return stats;
|
||||
}
|
||||
|
||||
private String getPlayerName(UUID uuid) {
|
||||
// 尝试获取在线玩家
|
||||
ServerPlayerEntity player = server.getPlayerManager().getPlayer(uuid);
|
||||
if (player != null) {
|
||||
return player.getName().getString();
|
||||
}
|
||||
|
||||
// 尝试从用户缓存获取 - 现在正确处理Optional
|
||||
Optional<GameProfile> profile = server.getUserCache().getByUuid(uuid);
|
||||
if (profile.isPresent()) {
|
||||
return profile.get().getName();
|
||||
}
|
||||
|
||||
return "Unknown";
|
||||
}
|
||||
|
||||
private void loadData() {
|
||||
if (!Files.exists(dataFile)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try (Reader reader = Files.newBufferedReader(dataFile)) {
|
||||
JsonObject root = JsonParser.parseReader(reader).getAsJsonObject();
|
||||
for (Map.Entry<String, JsonElement> entry : root.entrySet()) {
|
||||
UUID uuid = UUID.fromString(entry.getKey());
|
||||
playerData.put(uuid, GSON.fromJson(entry.getValue(), PlayerTimeData.class));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
PlayerTimeMod.LOGGER.error("无法加载玩家在线时间数据", e);
|
||||
}
|
||||
}
|
||||
|
||||
public void saveAll() {
|
||||
JsonObject root = new JsonObject();
|
||||
playerData.forEach((uuid, data) -> {
|
||||
root.add(uuid.toString(), GSON.toJsonTree(data));
|
||||
});
|
||||
|
||||
try (Writer writer = Files.newBufferedWriter(dataFile)) {
|
||||
GSON.toJson(root, writer);
|
||||
} catch (Exception e) {
|
||||
PlayerTimeMod.LOGGER.error("无法保存玩家在线时间数据", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void saveAsync(UUID uuid) {
|
||||
executor.execute(() -> {
|
||||
JsonObject root;
|
||||
try {
|
||||
if (Files.exists(dataFile)) {
|
||||
try (Reader reader = Files.newBufferedReader(dataFile)) {
|
||||
root = JsonParser.parseReader(reader).getAsJsonObject();
|
||||
}
|
||||
} else {
|
||||
root = new JsonObject();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
root = new JsonObject();
|
||||
}
|
||||
|
||||
PlayerTimeData data = playerData.get(uuid);
|
||||
if (data != null) {
|
||||
root.add(uuid.toString(), GSON.toJsonTree(data));
|
||||
}
|
||||
|
||||
try (Writer writer = Files.newBufferedWriter(dataFile)) {
|
||||
GSON.toJson(root, writer);
|
||||
} catch (Exception e) {
|
||||
PlayerTimeMod.LOGGER.error("无法保存" + uuid + "的在线时间数据", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static String formatTime(long seconds) {
|
||||
long hours = seconds / 3600;
|
||||
long minutes = (seconds % 3600) / 60;
|
||||
return String.format("%dh %02dm", hours, minutes);
|
||||
}
|
||||
|
||||
private static class PlayerTimeData {
|
||||
long totalTime = 0;
|
||||
long lastLogin = 0;
|
||||
RollingTimeWindow rolling30Days = new RollingTimeWindow(30);
|
||||
RollingTimeWindow rolling7Days = new RollingTimeWindow(7);
|
||||
}
|
||||
|
||||
public static class PlayerTimeStats {
|
||||
public long totalTime;
|
||||
public long last30Days;
|
||||
public long last7Days;
|
||||
}
|
||||
|
||||
private static class RollingTimeWindow {
|
||||
private final int days;
|
||||
private final List<TimeEntry> entries = new ArrayList<>();
|
||||
|
||||
public RollingTimeWindow(int days) {
|
||||
this.days = days;
|
||||
}
|
||||
|
||||
public void addPlayTime(long timestamp, long seconds) {
|
||||
entries.add(new TimeEntry(timestamp, seconds));
|
||||
cleanUp(timestamp);
|
||||
}
|
||||
|
||||
public long getTotalTime(long currentTime) {
|
||||
cleanUp(currentTime);
|
||||
return entries.stream().mapToLong(e -> e.seconds).sum();
|
||||
}
|
||||
|
||||
private void cleanUp(long currentTime) {
|
||||
long cutoff = currentTime - (days * 24 * 3600);
|
||||
entries.removeIf(entry -> entry.timestamp < cutoff);
|
||||
}
|
||||
|
||||
private static class TimeEntry {
|
||||
final long timestamp;
|
||||
final long seconds;
|
||||
|
||||
TimeEntry(long timestamp, long seconds) {
|
||||
this.timestamp = timestamp;
|
||||
this.seconds = seconds;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
111
src/main/java/com/example/playertime/WebServer.java
Normal file
111
src/main/java/com/example/playertime/WebServer.java
Normal file
@ -0,0 +1,111 @@
|
||||
package com.example.playertime;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.sun.net.httpserver.HttpServer;
|
||||
import com.sun.net.httpserver.HttpHandler;
|
||||
import com.sun.net.httpserver.HttpExchange;
|
||||
|
||||
import java.io.*;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.URL;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.*;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.*;
|
||||
|
||||
public class WebServer {
|
||||
private final HttpServer server;
|
||||
private final PlayerTimeTracker timeTracker;
|
||||
private final ExecutorService executor = Executors.newFixedThreadPool(4);
|
||||
private static final Map<String, String> MIME_TYPES = Map.of(
|
||||
"html", "text/html",
|
||||
"css", "text/css",
|
||||
"js", "application/javascript",
|
||||
"json", "application/json"
|
||||
);
|
||||
|
||||
public WebServer(PlayerTimeTracker timeTracker, int port) throws IOException {
|
||||
this.timeTracker = timeTracker;
|
||||
this.server = HttpServer.create(new InetSocketAddress(port), 0);
|
||||
setupContexts();
|
||||
}
|
||||
|
||||
private void setupContexts() {
|
||||
// API 端点
|
||||
// 在 WebServer.java 中修改 /api/stats 的处理
|
||||
server.createContext("/api/stats", exchange -> {
|
||||
if (!"GET".equals(exchange.getRequestMethod())) {
|
||||
sendResponse(exchange, 405, "Method Not Allowed");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 改为使用新的白名单统计方法
|
||||
Map<String, String> stats = timeTracker.getWhitelistedPlayerStats();
|
||||
String response = new Gson().toJson(stats);
|
||||
sendResponse(exchange, 200, response.getBytes(), "application/json");
|
||||
} catch (Exception e) {
|
||||
PlayerTimeMod.LOGGER.error("Failed to get stats", e);
|
||||
sendResponse(exchange, 500, "Internal Server Error");
|
||||
}
|
||||
});
|
||||
|
||||
// 静态文件服务
|
||||
server.createContext("/", exchange -> {
|
||||
try {
|
||||
String path = exchange.getRequestURI().getPath();
|
||||
if (path.equals("/")) path = "/index.html";
|
||||
|
||||
// 从资源目录加载文件
|
||||
String resourcePath = "assets/playertime/web" + path;
|
||||
InputStream is = getClass().getClassLoader().getResourceAsStream(resourcePath);
|
||||
|
||||
if (is == null) {
|
||||
sendResponse(exchange, 404, "Not Found");
|
||||
return;
|
||||
}
|
||||
|
||||
// 确定内容类型
|
||||
String extension = path.substring(path.lastIndexOf('.') + 1);
|
||||
String contentType = MIME_TYPES.getOrDefault(extension, "text/plain");
|
||||
|
||||
// 读取文件内容
|
||||
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
|
||||
byte[] data = new byte[1024];
|
||||
int nRead;
|
||||
while ((nRead = is.read(data, 0, data.length)) != -1) {
|
||||
buffer.write(data, 0, nRead);
|
||||
}
|
||||
buffer.flush();
|
||||
|
||||
sendResponse(exchange, 200, buffer.toByteArray(), contentType);
|
||||
} catch (Exception e) {
|
||||
PlayerTimeMod.LOGGER.error("Failed to serve resource", e);
|
||||
sendResponse(exchange, 500, "Internal Server Error");
|
||||
}
|
||||
});
|
||||
|
||||
server.setExecutor(executor);
|
||||
}
|
||||
|
||||
private void sendResponse(HttpExchange exchange, int code, String response) throws IOException {
|
||||
sendResponse(exchange, code, response.getBytes(StandardCharsets.UTF_8), "text/plain");
|
||||
}
|
||||
|
||||
private void sendResponse(HttpExchange exchange, int code, byte[] response, String contentType) throws IOException {
|
||||
exchange.getResponseHeaders().set("Content-Type", contentType);
|
||||
exchange.sendResponseHeaders(code, response.length);
|
||||
try (OutputStream os = exchange.getResponseBody()) {
|
||||
os.write(response);
|
||||
}
|
||||
}
|
||||
|
||||
public void start() {
|
||||
server.start();
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
server.stop(0);
|
||||
executor.shutdown();
|
||||
}
|
||||
}
|
77
src/main/resources/assets/playertime/web/css/style.css
Normal file
77
src/main/resources/assets/playertime/web/css/style.css
Normal file
@ -0,0 +1,77 @@
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background-color: #f5f5f5;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: #2c3e50;
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.controls {
|
||||
margin-bottom: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
button {
|
||||
background-color: #3498db;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
transition: background-color 0.3s;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background-color: #2980b9;
|
||||
}
|
||||
|
||||
.stats-container {
|
||||
background-color: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
th, td {
|
||||
padding: 12px 15px;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid #ddd;
|
||||
}
|
||||
|
||||
th {
|
||||
background-color: #3498db;
|
||||
color: white;
|
||||
}
|
||||
|
||||
tr:hover {
|
||||
background-color: #f1f1f1;
|
||||
}
|
||||
|
||||
tr:nth-child(even) {
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
table {
|
||||
display: block;
|
||||
overflow-x: auto;
|
||||
}
|
||||
}
|
32
src/main/resources/assets/playertime/web/index.html
Normal file
32
src/main/resources/assets/playertime/web/index.html
Normal file
@ -0,0 +1,32 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>[在线时间] 玩家在线时间</title>
|
||||
<link rel="stylesheet" href="css/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>KSE玩家在线时间统计</h1>
|
||||
<div class="controls">
|
||||
<button id="refresh-btn">刷新数据</button>
|
||||
<p class="info-note">仅跟踪和显示列入白名单的玩家</p>
|
||||
</div>
|
||||
<div class="stats-container">
|
||||
<table id="stats-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>玩家</th>
|
||||
<th>总计时间</th>
|
||||
<th>最近 30 天</th>
|
||||
<th>最近 7 天</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<script src="js/app.js"></script>
|
||||
</body>
|
||||
</html>
|
65
src/main/resources/assets/playertime/web/js/app.js
Normal file
65
src/main/resources/assets/playertime/web/js/app.js
Normal file
@ -0,0 +1,65 @@
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const refreshBtn = document.getElementById('refresh-btn');
|
||||
const statsTable = document.getElementById('stats-table').getElementsByTagName('tbody')[0];
|
||||
|
||||
// 初始加载数据
|
||||
loadStats();
|
||||
|
||||
// 刷新按钮点击事件
|
||||
refreshBtn.addEventListener('click', loadStats);
|
||||
|
||||
// 加载统计数据
|
||||
function loadStats() {
|
||||
fetch('/api/stats')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
updateTable(data);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error fetching stats:', error);
|
||||
alert('Failed to load player stats. Check console for details.');
|
||||
});
|
||||
}
|
||||
|
||||
// 更新表格数据
|
||||
function updateTable(statsData) {
|
||||
const statsTable = document.getElementById('stats-table').getElementsByTagName('tbody')[0];
|
||||
statsTable.innerHTML = '';
|
||||
|
||||
// 新数据格式是 { "玩家名": "统计信息", ... }
|
||||
Object.entries(statsData).forEach(([playerName, statString]) => {
|
||||
const row = statsTable.insertRow();
|
||||
|
||||
// 玩家名列
|
||||
const nameCell = row.insertCell(0);
|
||||
nameCell.textContent = playerName;
|
||||
|
||||
// 解析统计信息
|
||||
const stats = {};
|
||||
statString.split(" | ").forEach(part => {
|
||||
const [label, value] = part.split(": ");
|
||||
stats[label.trim()] = value;
|
||||
});
|
||||
|
||||
// 总时长列
|
||||
const totalCell = row.insertCell(1);
|
||||
totalCell.textContent = stats["总时长"];
|
||||
|
||||
// 30天列
|
||||
const thirtyCell = row.insertCell(2);
|
||||
thirtyCell.textContent = stats["30天"];
|
||||
|
||||
// 7天列
|
||||
const sevenCell = row.insertCell(3);
|
||||
sevenCell.textContent = stats["7天"];
|
||||
});
|
||||
}
|
||||
|
||||
// 辅助函数:将"Xh Ym"格式的时间转换为分钟数
|
||||
function parseTime(timeStr) {
|
||||
const [hPart, mPart] = timeStr.split(' ');
|
||||
const hours = parseInt(hPart.replace('h', ''));
|
||||
const minutes = parseInt(mPart.replace('m', ''));
|
||||
return hours * 60 + minutes;
|
||||
}
|
||||
});
|
24
src/main/resources/fabric.mod.json
Normal file
24
src/main/resources/fabric.mod.json
Normal file
@ -0,0 +1,24 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"id": "playertime",
|
||||
"version": "${version}",
|
||||
"name": "玩家在线时长统计Mod",
|
||||
"description": "",
|
||||
"authors": [],
|
||||
"contact": {},
|
||||
"license": "MIT",
|
||||
"environment": "server",
|
||||
"entrypoints": {
|
||||
"main": [
|
||||
"com.example.playertime.PlayerTimeMod"
|
||||
]
|
||||
},
|
||||
"mixins": [
|
||||
"playertime.mixins.json"
|
||||
],
|
||||
"depends": {
|
||||
"fabricloader": ">=${loader_version}",
|
||||
"fabric": "*",
|
||||
"minecraft": "${minecraft_version}"
|
||||
}
|
||||
}
|
8
src/main/resources/playertime.mixins.json
Normal file
8
src/main/resources/playertime.mixins.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"required": true,
|
||||
"package": "com.example.playertime.mixins",
|
||||
"compatibilityLevel": "JAVA_21",
|
||||
"injectors": {
|
||||
"defaultRequire": 1
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user