Compare commits

...

227 Commits
beta ... 1.20.1

Author SHA1 Message Date
018d0dfc33 Feat: 更新版本
Some checks failed
Build / build (push) Failing after 1m34s
2026-06-09 10:23:25 +08:00
mlus
ac20ad327c Feat: 添加了Mek支持
Some checks failed
Build / build (push) Has been cancelled
Merge pull request #172 from mlus-asuka/fix/169-bounded-thread-pool

Fix/169 bounded thread pool
2026-06-09 10:22:39 +08:00
mlus
e15c9b335e
Merge pull request #172 from mlus-asuka/fix/169-bounded-thread-pool
Fix/169 bounded thread pool
2026-05-10 00:06:51 +08:00
mlus
b6de595c41 Merge branch '1.20.1' of https://github.com/mlus-asuka/PlayerSync into 1.20.1 2026-05-10 00:00:12 +08:00
mlus
8df3b97356 fix #169 - bounded thread pool
Replace unbounded CachedThreadPool with bounded ThreadPoolExecutor

to prevent memory leaks and server crashes under high load.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 23:59:55 +08:00
mlus
9ce83763c9
Merge pull request #170 from mlus-asuka/dependabot/github_actions/gradle/actions-6
Bump gradle/actions from 5 to 6
2026-04-21 17:06:14 +08:00
dependabot[bot]
c4e18e61a8
Bump gradle/actions from 5 to 6
Bumps [gradle/actions](https://github.com/gradle/actions) from 5 to 6.
- [Release notes](https://github.com/gradle/actions/releases)
- [Commits](https://github.com/gradle/actions/compare/v5...v6)

---
updated-dependencies:
- dependency-name: gradle/actions
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-01 21:02:27 +00:00
mlus
8ff5d357a0
Merge pull request #167 from mlus-asuka/dependabot/github_actions/actions/upload-artifact-7
Bump actions/upload-artifact from 6 to 7
2026-03-04 15:50:28 +08:00
dependabot[bot]
235d95144f
Bump actions/upload-artifact from 6 to 7
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 6 to 7.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v6...v7)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-01 16:26:39 +00:00
mlus
4e4ad80a95
Merge pull request #166 from mlus-asuka/1.20.1-dev
SQL syntax fix about Database name
2026-02-24 00:24:45 +08:00
mlus
32f2e2d75e fix #165 2026-02-24 00:22:40 +08:00
mlus
5764e85647
Merge pull request #164 from LeisureTimeDock/1.20.1
[Backport 1.20.1]Fix #151 #160
2026-02-06 16:13:07 +08:00
86d6393c87 update version 2026-02-06 11:21:05 +08:00
3944Realms
5632be3d3d
Change KICK_WHEN_ALREADY_ONLINE to final 2026-02-06 11:09:32 +08:00
3944Realms
1c5f3cddd4
Add kick option for players already online 2026-02-06 11:02:04 +08:00
3944Realms
a367eb0e3e
Update JdbcConfig.java 2026-02-06 11:01:07 +08:00
mlus
a47bc4bf80
Merge pull request #159 from mlus-asuka/dependabot/github_actions/actions/upload-artifact-6
Bump actions/upload-artifact from 5 to 6
2026-01-02 01:17:28 +08:00
mlus
c3750da764
Merge pull request #158 from mlus-asuka/dependabot/github_actions/korthout/backport-action-4
Bump korthout/backport-action from 3 to 4
2026-01-02 01:17:14 +08:00
dependabot[bot]
865926bc54
Bump actions/upload-artifact from 5 to 6
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 5 to 6.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-01 16:18:29 +00:00
dependabot[bot]
66808f2242
Bump korthout/backport-action from 3 to 4
Bumps [korthout/backport-action](https://github.com/korthout/backport-action) from 3 to 4.
- [Release notes](https://github.com/korthout/backport-action/releases)
- [Commits](https://github.com/korthout/backport-action/compare/v3...v4)

---
updated-dependencies:
- dependency-name: korthout/backport-action
  dependency-version: '4'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-01 16:18:24 +00:00
mlus
ce07fed83d fix init advancement 2025-12-07 00:45:50 +08:00
mlus
5583424e22
Merge pull request #149 from mlus-asuka/dependabot/github_actions/actions/checkout-6
Bump actions/checkout from 5 to 6
2025-12-02 18:30:50 +08:00
dependabot[bot]
4ac5d77345
Bump actions/checkout from 5 to 6
Bumps [actions/checkout](https://github.com/actions/checkout) from 5 to 6.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-01 19:25:35 +00:00
mlus
dfabb42c6b
Merge pull request #147 from mlus-asuka/1.20.1-dev
Fix sync order for mod support
2025-11-30 01:04:08 +08:00
mlus
f4584d58b5 :) 2025-11-30 01:01:49 +08:00
mlus
3291fc54b2 backpack dirty mark test 2025-11-26 15:42:29 +08:00
mlus
3e70f4b801
Merge pull request #146 from Fugit-5414/feature/some-improvements
Related to #136
2025-11-24 23:43:59 +08:00
Fugit-5414
5696272781 Related to #136 2025-11-24 22:11:13 +08:00
mlus
b062331cce
Merge pull request #145 from Fugit-5414/fix/sync-failed-when-player-health-is-zero
Fixes #144
2025-11-24 11:59:04 +08:00
Fugit-5414
a05b0b0375 Fixes #144 2025-11-23 22:17:27 +08:00
mlus
d78c84d8ce
Merge pull request #143 from mlus-asuka/1.20.1-dev
2.1.4 update
2025-11-22 23:01:31 +08:00
mlus
f39a64bf14 2.1.4 update 2025-11-22 22:59:23 +08:00
mlus
8b112ecc86
Merge pull request #141 from Fugit-5414/fix/wrong-player-status-after-restart-from-crashes
Fixes #140
2025-11-22 22:37:09 +08:00
Fugit-5414
176d123f4e Fixes #140 2025-11-22 22:30:46 +08:00
mlus
63a21ce5cc
Merge pull request #139 from Fugit-5414/fix/Curios-nbtException
Fixes #138
2025-11-20 23:24:50 +08:00
Fugit-5414
fe1e7584d4 Fixes #138 2025-11-20 23:04:48 +08:00
mlus
c2d5d37d30 auto close prepared statement 2025-11-20 12:18:26 +08:00
mlus
1b4cfe4e39
Merge pull request #137 from mlus-asuka/1.20.1-dev
1.20.1 dev
2025-11-20 11:48:34 +08:00
mlus
5164900bad
Merge pull request #135 from Fugit-5414/fix/critical-blocking-bypass
Fixes #134
2025-11-20 11:33:14 +08:00
Fugit-5414
50b467c780 Fixes #134 2025-11-20 01:46:19 +08:00
mlus
df4dbf6884
Merge pull request #129 from mlus-asuka/dependabot/github_actions/actions/upload-artifact-5
Bump actions/upload-artifact from 4 to 5
2025-11-03 11:47:14 +08:00
dependabot[bot]
6781b7ad71
Bump actions/upload-artifact from 4 to 5
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4 to 5.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-01 16:17:18 +00:00
mlus
3cfa05368f remove unnecessary logger 2025-10-18 01:16:33 +08:00
mlus
610f652141 keep socket waiting test 2025-10-15 20:36:54 +08:00
mlus
9b889d2458
Merge pull request #126 from mlus-asuka/1.20.1-dev
Chat Sync reconnect system and initialization bug fix
2025-10-14 16:06:11 +08:00
mlus
08e32a675e line 155 make method return so curios data won't save at the first time 2025-10-14 16:00:21 +08:00
mlus
cfa9387598 heartbeat 2025-10-14 13:45:47 +08:00
mlus
a5041917a4 chat sync reconnect system 2025-10-14 13:09:43 +08:00
mlus
05643bd0b4
Merge pull request #124 from mlus-asuka/dependabot/github_actions/gradle/actions-5
Bump gradle/actions from 4 to 5
2025-10-02 17:23:51 +08:00
dependabot[bot]
933cd48c03
Bump gradle/actions from 4 to 5
Bumps [gradle/actions](https://github.com/gradle/actions) from 4 to 5.
- [Release notes](https://github.com/gradle/actions/releases)
- [Commits](https://github.com/gradle/actions/compare/v4...v5)

---
updated-dependencies:
- dependency-name: gradle/actions
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-01 16:32:46 +00:00
mlus
9055e9d342
Merge pull request #117 from mlus-asuka/1.20.1-dev
may fix #111
2025-09-16 22:12:55 +08:00
mlus
befa36a303 may fix #111 2025-09-16 22:11:25 +08:00
mlus
7754186d12
Merge pull request #116 from mlus-asuka/1.20.1-dev
clear curios before restore?
2025-09-10 16:11:02 +08:00
mlus
c28a312f3c clear curios before restore? 2025-09-10 16:05:54 +08:00
mlus
4d27f3a2d6
Merge pull request #114 from mlus-asuka/dependabot/github_actions/actions/setup-java-5
Bump actions/setup-java from 4 to 5
2025-09-02 19:01:32 +08:00
mlus
2cea1068dd
Merge pull request #113 from mlus-asuka/dependabot/github_actions/actions/checkout-5
Bump actions/checkout from 4 to 5
2025-09-02 19:01:21 +08:00
dependabot[bot]
86aae9534c
Bump actions/setup-java from 4 to 5
Bumps [actions/setup-java](https://github.com/actions/setup-java) from 4 to 5.
- [Release notes](https://github.com/actions/setup-java/releases)
- [Commits](https://github.com/actions/setup-java/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/setup-java
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-02 10:56:41 +00:00
dependabot[bot]
77704d6431
Bump actions/checkout from 4 to 5
Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 5.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-02 10:41:15 +00:00
mlus
db318df85e
Merge pull request #108 from EoD/make-configs-final-1.20.1
[1.20.1] make all config variables final
2025-08-01 11:54:15 +08:00
EoD
acfef0ff7e make all config variables final
the classes never change, even if the config is modified. Only the
values we read with get() inside the classes change.
2025-07-31 23:58:12 +00:00
mlus
ca739b0b68
Merge pull request #105 from EoD/add-data_version-1.20.1
[1.20.1] add new data_version column to server_info table
2025-07-31 12:30:21 +08:00
EoD
36847cc025 add new data_version column to server_info table 2025-07-30 23:25:58 +00:00
mlus
23d96e128e
Merge pull request #103 from EoD/improve-sql-functions
Some SQL-related improvements
2025-07-30 12:02:56 +08:00
EoD
4fe13bd24d extract addColumnIfNotExists into separate method 2025-07-29 20:36:33 +00:00
EoD
7f06aa7511 reformat insert into server_info for readability
Use the new format capabilities on SQL queries to make the insert more
readable.
2025-07-29 20:30:35 +00:00
EoD
e1ac7adb11 allow format strings within SQL queries
This makes SQL queries more readable in some cases
2025-07-29 20:30:35 +00:00
EoD
8f77a96544 clarify executeUpdate variants with and without db 2025-07-29 20:30:35 +00:00
mlus
7a3363592e
Merge pull request #99 from EoD/make-queryresult-autoclosable
make QueryResult AutoClosable
2025-07-29 18:35:47 +08:00
mlus
cd13b282e2
Merge pull request #98 from EoD/remove-duplicate-update
remove duplicate UPDATE server_info
2025-07-29 18:33:54 +08:00
mlus
d934ceff5a
Merge pull request #97 from EoD/log-advancement-column-type
log the advancement's data type before altering
2025-07-29 18:29:32 +08:00
EoD
ad76e0e311 make QueryResult AutoClosable
This allows QueryResults to be used within a try() block without
explicitely closing them.
2025-07-28 22:12:47 +00:00
EoD
0e96107416 remove duplicate UPDATE server_info
the UPDATE is already happening in in the INSERT statement above
2025-07-28 22:06:45 +00:00
EoD
54cbb9c9a8 log the advancement's data type before altering
it seems that sometimes this is triggered on an existing database. This
should help identifying what is happening in these cases.
2025-07-28 21:56:24 +00:00
mlus
2ec7fec89e
Merge pull request #93 from EoD/fix-string-to-map
fix string to map generation with base64 encoding
2025-07-12 13:14:19 +08:00
mlus
0d1a26e348
Merge pull request #91 from EoD/remove-unnecessary-import-1.20.1
[1.20.1] remove unnecessary deserializeString import
2025-07-11 12:37:33 +08:00
mlus
9ce7f3c38f
Merge pull request #90 from EoD/create-placeholder-within-curios
unify item creation in curios and normal inventory
2025-07-11 12:37:05 +08:00
EoD
a70605a8b6 unify item creation in curios and normal inventory
this also allows creation of placeholders within curios containers
2025-07-10 22:33:15 +00:00
EoD
53bdfe2309 simplify and exit early in stringToGenericMap 2025-07-10 22:07:55 +00:00
EoD
228b835c2a unify both stringToMap functions 2025-07-10 22:07:55 +00:00
EoD
de324a23be fix string to map generation with base64 encoding
The new base64 encoding uses "=" characters as part of its encoding.
The previous code split those and trimmed them afterwards, making the
base64 not consistent with the rest of the code that assumed "=" are
still present, like "B64:e30=".
2025-07-10 22:07:03 +00:00
EoD
4ae1954b29 remove unnecessary deserializeString import 2025-07-10 19:26:23 +00:00
mlus
a382b0105a
Merge pull request #88 from EoD/upgrade-items-between-versions-1.20.1
[1.20.1] properly upgrade items with newer MC versions
2025-07-07 00:07:22 +08:00
EoD
a1e1616eac properly upgrade items with newer MC versions 2025-07-06 16:02:08 +00:00
mlus
d29234109b
Merge pull request #85 from EoD/organize-imports-1.20.1
[1.20.1] optimize all imports
2025-06-18 12:28:35 +08:00
mlus
7a47c58316
Merge pull request #84 from EoD/sync-save-logic
use same save logic in 1.20.1 and 1.20.4
2025-06-18 12:28:07 +08:00
EoD
7787c79aec add vscode setting for IntelliJ-like imports 2025-06-17 23:53:15 +00:00
EoD
1193a17010 optimize all imports 2025-06-17 23:53:10 +00:00
EoD
51200c28b3 use same save logic in 1.20.1 and 1.20.4 2025-06-17 21:20:21 +00:00
mlus
2d891db071
Merge pull request #79 from mlus-asuka/1.20.1-dev
Fix error when no player in server
2025-06-06 13:02:22 +08:00
mlus
d766febb11 Fix error when no player in server 2025-06-06 12:59:52 +08:00
mlus
a5f0cf1978
Merge pull request #77 from mlus-asuka/1.20.1-dev
Full tested ChatSync Feature
2025-06-05 03:34:54 +08:00
mlus
8f2b6d84b1
Merge branch '1.20.1' into 1.20.1-dev 2025-06-05 03:30:19 +08:00
mlus
a774688d45 Full tested ChatSync Feature 2025-06-05 03:28:24 +08:00
mlus
b1a11f2ba9
Merge pull request #75 from EoD/bump-version
bump mod version to 2.1.1
2025-05-19 13:08:47 +08:00
EoD
e2b90ccf98 bump mod version to 2.1.1 2025-05-18 22:02:56 +00:00
mlus
aca7900890
Merge pull request #73 from EoD/add-pat-to-backport
use PAT in backport action
2025-05-19 01:25:58 +08:00
EoD
e0f0b51851 use PAT in backport action 2025-05-18 17:24:00 +00:00
mlus
d873711a40
Merge pull request #70 from EoD/fix-xp-sync
fix XP being lost or duplicated
2025-05-18 12:18:07 +08:00
mlus
b591d994c1
Merge pull request #67 from EoD/add-backport-prs
add action to automatically backport PRs
2025-05-18 12:16:47 +08:00
mlus
98e0cc3f60
Merge pull request #68 from EoD/fix-language-files
remove unused override in the language files
2025-05-18 12:15:49 +08:00
EoD
d83bad5a33 fix XP being lost or duplicated
The current calculation did not work for larger amounts of levels
and either removed or added levels unintentionally.
2025-05-17 19:32:20 +00:00
EoD
319fe678f3 remove unused override in the language files 2025-05-17 19:21:25 +00:00
EoD
5ba7cc2972 add action to automatically backport PRs 2025-05-17 19:04:35 +00:00
mlus
d465a724f5 language file fix 2025-05-15 18:03:40 +08:00
mlus
606b9e1c1e
Merge pull request #62 from EoD/version-compat
encode unknown items using Paper
2025-05-05 02:03:03 +08:00
mlus
110a2baaa5
Merge pull request #64 from EoD/automatic-releases
Automatic Mod Releases
2025-05-05 01:37:18 +08:00
EoD
9ebcb65233 automatically release mod when pushing tags 2025-05-04 17:17:04 +00:00
EoD
0e8527a9f1 rename build-1.20.yml to build.yml 2025-05-04 16:54:17 +00:00
EoD
9f256df298 bump version to 2.1.0 2025-05-04 16:48:57 +00:00
EoD
6ac6f297af encode unknown items using Paper
This allows using PlayerSync with different minecraft versions and
even different sets of mods.

All unknown items are replaced by Paper with its original NBT data
encoded into the paper item.
2025-05-04 16:48:57 +00:00
mlus
ba879dabdb
Merge pull request #63 from EoD/disable-chat-sync-by-default
disable chat sync by default
2025-05-04 20:29:22 +08:00
mlus
39b69424e8 so it is 2.0.0 release 2025-05-04 18:40:39 +08:00
EoD
14ea54fcf3 disable chat sync by default 2025-05-03 21:50:17 +00:00
mlus
74f348c2c4
Merge pull request #61 from EoD/bump-version
bump mod version to 2.0.0
2025-05-04 00:34:54 +08:00
EoD
20e1759b46 bump mod version to 2.0.0 2025-05-03 16:30:01 +00:00
mlus
cfe6467b36
Merge pull request #60 from EoD/run-ci-on-all-1.x-branches
run build CI on all 1.xxx branches
2025-05-04 00:27:42 +08:00
EoD
09b33848e9 run build CI on all 1.xxx branches 2025-05-03 16:21:00 +00:00
mlus
d9ea1e97e2
Merge pull request #59 from EoD/fix-deprecated-ci
Update deprecated Gradle Actions
2025-05-04 00:16:00 +08:00
EoD
421c03a47c remove deprecated "argument" parameter from action
See
755ed7db09/docs/deprecation-upgrade-guide.md
2025-05-03 16:12:53 +00:00
mlus
a9c4df2a61
Merge pull request #58 from EoD/migrate-to-moddevgradle
Migrate to ModDevGradle legacy to embed JDCB driver
2025-05-04 00:05:26 +08:00
EoD
b6e5a5b5af update deprecated gradle actions
See
755ed7db09/docs/deprecation-upgrade-guide.md
2025-05-03 16:04:53 +00:00
mlus
162df73189
Merge pull request #57 from EoD/fix-advancements-on-brand-new-server
fix advancement restore on brand new servers
2025-05-03 11:17:18 +08:00
mlus
19559bf9e2
Merge pull request #55 from EoD/fix-chat-config-being-ignored
fix chat sync always being enabled
2025-05-03 11:15:59 +08:00
mlus
dd5c9c6ae8
Merge pull request #54 from EoD/fix-advancements-for-new-players
fix advancement sync for new players
2025-05-03 11:14:36 +08:00
EoD
ce0e173a9e register JDBC driver to work around Forge bug 2025-05-02 22:40:39 +00:00
EoD
033c2b8348 use jarJar to embed the JDBC driver
This does not cause conflicts with other mods that do the same and it
reduces the dependency on the "jdbc mods" that are out there.
2025-05-02 22:40:39 +00:00
EoD
8869e26f48 migrate from ForgeGradle to ModDevGradle legacy 2025-05-02 22:40:39 +00:00
EoD
284a1caf44 remove NotNull annotations 2025-05-02 22:38:36 +00:00
EoD
ba33d5271b fix advancement restore on brand new servers
On a brand new server, there is no advancements directory throwing an
IOException if we try to write a file to it.
2025-05-02 21:19:34 +00:00
EoD
5a3e157879 fix chat sync always being enabled
reading CHAT_SYNC immediately within FMLCommonSetupEvent can lead to
timing issues that the default value instead of the real config value is
being returned.

Moving them within event.enqueueWork() fixes the timing issue.
2025-05-02 20:32:29 +00:00
EoD
0cbca7cfd8 add logging for chat server 2025-05-02 20:32:29 +00:00
EoD
a1d1737d04 fix advancement sync for new players 2025-05-02 19:16:58 +00:00
mlus
f43c47f78d
Merge pull request #53 from EoD/advancement-sync-optional
make advancement sync optional
2025-05-03 02:02:40 +08:00
EoD
44eb3321b4 make advancement sync optional 2025-05-02 17:40:26 +00:00
mlus
fe386fb4ac
Merge pull request #42 from EoD/fix-advancements
fix storing and restoring of advancements to/from json
2025-05-02 11:07:07 +08:00
mlus
82cffe1c8a
Merge pull request #51 from EoD/fix-docker-compose
fix docker-compose database access
2025-05-02 11:05:21 +08:00
EoD
3f0172e185 use volume for docker-compose db to persist data 2025-05-01 18:42:58 +00:00
EoD
29df497980 fix docker-compose database access 2025-05-01 18:42:42 +00:00
EoD
7ece814357 fix advancement json restore
Previously, the json was written too late and never reloaded.
This commit moves the advancement restoration from the PlayerLoggedInEvent
to the earlier onDatapackSyncEvent.
At the same time, it forces a reload of the json files, making sure the
client is informed about the update advancements.
2025-05-01 17:05:03 +00:00
EoD
37d0eb2931 fix storing advancement json on dedicated servers 2025-05-01 17:05:03 +00:00
EoD
94433229b7 read level-name for servers from WorldData 2025-05-01 17:05:02 +00:00
mlus
11d2c68a3d
Merge pull request #50 from EoD/add-docker-compose-for-mysql
Add docker compose for MySQL setup
2025-05-02 00:59:58 +08:00
EoD
fdca2650ba readme: add section on how to setup a dev env 2025-05-01 16:59:05 +00:00
EoD
63ff76353d add docker compose file for MySQL database 2025-05-01 16:58:37 +00:00
mlus
24933718b5
Merge pull request #49 from EoD/add-issue-tracker
add issue tracker to mods.toml
2025-05-02 00:37:23 +08:00
mlus
c1778a956c fix error symbol 2025-05-02 00:35:47 +08:00
EoD
39239fbb64 add issue tracker to mods.toml 2025-05-01 16:34:23 +00:00
mlus
e5ba5cadac
Merge pull request #48 from mlus-asuka/dependabot/github_actions/gradle/gradle-build-action-3
Bump gradle/gradle-build-action from 2 to 3
2025-05-02 00:29:59 +08:00
mlus
0b68ad67a8
Merge pull request #47 from mlus-asuka/dependabot/github_actions/gradle/wrapper-validation-action-3
Bump gradle/wrapper-validation-action from 1 to 3
2025-05-02 00:29:51 +08:00
mlus
ee598cf6a9
Merge pull request #46 from mlus-asuka/dependabot/github_actions/actions/setup-java-4
Bump actions/setup-java from 3 to 4
2025-05-02 00:29:41 +08:00
mlus
b6f76a6af2
Merge pull request #39 from EoD/add-database-compat-flag
add configuration for legacy serialization
2025-05-02 00:28:27 +08:00
dependabot[bot]
7ea66ed8af
Bump gradle/gradle-build-action from 2 to 3
Bumps [gradle/gradle-build-action](https://github.com/gradle/gradle-build-action) from 2 to 3.
- [Release notes](https://github.com/gradle/gradle-build-action/releases)
- [Commits](https://github.com/gradle/gradle-build-action/compare/v2...v3)

---
updated-dependencies:
- dependency-name: gradle/gradle-build-action
  dependency-version: '3'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-01 16:27:53 +00:00
dependabot[bot]
419fa46894
Bump gradle/wrapper-validation-action from 1 to 3
Bumps [gradle/wrapper-validation-action](https://github.com/gradle/wrapper-validation-action) from 1 to 3.
- [Release notes](https://github.com/gradle/wrapper-validation-action/releases)
- [Commits](https://github.com/gradle/wrapper-validation-action/compare/v1...v3)

---
updated-dependencies:
- dependency-name: gradle/wrapper-validation-action
  dependency-version: '3'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-01 16:27:52 +00:00
dependabot[bot]
b7c9d73ff7
Bump actions/setup-java from 3 to 4
Bumps [actions/setup-java](https://github.com/actions/setup-java) from 3 to 4.
- [Release notes](https://github.com/actions/setup-java/releases)
- [Commits](https://github.com/actions/setup-java/compare/v3...v4)

---
updated-dependencies:
- dependency-name: actions/setup-java
  dependency-version: '4'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-01 16:27:51 +00:00
mlus
b35631430f
Merge pull request #44 from EoD/add-dependabot-config
Create dependabot.yml
2025-05-02 00:27:12 +08:00
mlus
4b7a5dfc1c
Merge pull request #38 from EoD/fix-sophisticated-table-creation
fix database setup without sophisticated backpack
2025-05-02 00:25:57 +08:00
mlus
08e73d00da
Merge pull request #37 from EoD/fix-sophisticated-backpack
fix hidden NoClassDefFoundError
2025-05-02 00:25:03 +08:00
mlus
1a0742828b
Merge pull request #43 from EoD/fix-armor-dupe
fix armor duping
2025-05-02 00:23:02 +08:00
mlus
90731c56c8
Merge pull request #45 from EoD/improve-dependencies
Improve dependencies
2025-05-02 00:21:42 +08:00
EoD
32856ccd67 fix hidden NoClassDefFoundError
net.p3pp3rf1y.sophisticatedbackpacks throws a NoClassDefFoundError when
sophisticated backpacks is not installed.
This exception never reaches the logs for unknown reasons.

Checking explicitly for ModList.get().isLoaded() ensures that the mod is
loaded.

Fixes regression of 439c7ee5bb
2025-05-01 16:16:42 +00:00
EoD
a2f7d13877 closes connection on sophisticated backpack 2025-05-01 16:15:54 +00:00
EoD
e8abf6a360 fix database setup without sophisticated backpack
Fixes SQLException:
java.sql.SQLSyntaxErrorException: Table 'playersync.backpack_data' doesn't exist

Fixes regression of 9ee7f9a95a
2025-05-01 16:15:54 +00:00
EoD
0fb6bb81e1 fix armor dupe bug
The continue "skipped" the armor entries in the database instead of
writing an explicit "air" item into the slot.

When restoring, only existing entries are being restored, all other
items are left untouched. Allowing to dupe items in armor slots.
2025-05-01 16:14:46 +00:00
EoD
8c13de490e bump MySQL dependency to 8.0.33+20230506
Fixed "incompatible mod set" warnings on server list

See https://github.com/kosmolot-mods/minecraft-mysql-jdbc/releases/tag/8.0.30%2B20220916
2025-05-01 16:11:33 +00:00
EoD
2331485970 make sure the MySQL dependency is installed 2025-05-01 16:09:57 +00:00
EoD
11b5d26143 fix gradle mod dependencies
This fixes ./gradlew runServer
2025-05-01 16:09:57 +00:00
mlus
92e95a94a4
Merge pull request #36 from EoD/trace-logs
add trace logs for all SQL queries
2025-05-01 21:04:11 +08:00
mlus
d38383a22d
Merge pull request #35 from EoD/update-gradlew
bump Gradle to Forge 47.4.0 and update mod config
2025-05-01 21:03:43 +08:00
mlus
388c8b3558
Merge pull request #34 from EoD/fix-deprecations
fix FMLJavaModLoadingContext deprecations
2025-05-01 21:02:59 +08:00
mlus
d1e285acd7
Merge pull request #33 from EoD/vscode-gitignore
ignore vscode generated files
2025-05-01 21:02:27 +08:00
mlus
e7bb8bcd55
Merge pull request #32 from EoD/patch-1
ci: fix outdated actions/upload-artifact action and enable it for PRs
2025-05-01 21:02:09 +08:00
EoD
b784341fe5 Create dependabot.yml 2025-04-27 21:09:32 +00:00
EoD
44a3e9ca8c add configuration for legacy serialization 2025-04-25 23:31:43 +00:00
EoD
a510b091db add trace logs for all SQL queries
Can be enabled by starting minecraft with
-Dforge.logging.console.level=trace
2025-04-25 20:40:16 +00:00
EoD
795ca6cedf fix clients to require PlayerSync to be installed 2025-04-25 20:36:40 +00:00
EoD
85707a0854 fix FMLJavaModLoadingContext deprecations 2025-04-23 22:39:44 +00:00
EoD
b1cea45621 ignore vscode generated files
These can be generated with ./gradlew genVSCodeRuns
2025-04-23 22:37:51 +00:00
EoD
1d426a5e13 ci: remove unnecessary chmod +x 2025-04-23 22:16:17 +00:00
EoD
086374eeb1 bump gradlew and update mod config 2025-04-23 22:10:48 +00:00
EoD
15081d3320
ci: run build also on pull requests 2025-04-24 00:00:01 +02:00
EoD
742914d474
ci: fix outdated actions/upload-artifact action 2025-04-23 23:56:46 +02:00
mlus
cc687a8ea0
Merge pull request #31 from mchivelli/1.20.1
Fixed Curios problems
2025-03-27 18:13:00 +08:00
mchivelli
5d1a166dcf
Merge branch '1.20.1' into 1.20.1 2025-03-21 23:49:31 +01:00
paulm
9ee7f9a95a Fixed Curios problems 2025-03-21 20:03:42 +01:00
mlus
a7f1373713 1.3.5 release 2025-03-21 19:42:08 +08:00
mlus
d346bd36ae
Merge pull request #30 from mchivelli/1.20.1
Added Sophisticated Backpack Mod compatibility
2025-03-20 22:27:57 +08:00
paulm
439c7ee5bb Addeed Sophisticated Backpack Mod compatibility 2025-03-20 05:26:15 +01:00
mlus
244e764c74 1.3.4 release 2024-10-11 17:20:46 +08:00
mlus
721e013473 missing s 2024-10-11 13:57:28 +08:00
mlus
e22b21c826 Update left_hand and cursor stack sync 2024-10-11 13:25:33 +08:00
mlus
a77fc52da9 fix #23 2024-10-10 18:44:36 +08:00
mlus
9332ac6353 try to fix #23 2024-10-10 13:07:58 +08:00
mlus
bb45488186 fix #22 2024-09-24 21:46:12 +08:00
mlus
1ec9be4e5a reduce frequency of updating server info 2024-08-05 02:24:04 +08:00
mlus
053758e6cc And for update 2024-08-04 17:51:07 +08:00
mlus
bd4694e44b DataBase Fixed 2024-08-04 17:46:38 +08:00
mlus
14686a930f build fix 2024-08-04 14:26:01 +08:00
mlus
50e146d7fe Chat Sync performance enhancement 2024-08-04 14:20:25 +08:00
mlus
201bf95ff5 security vulnerability fix 2024-05-19 12:39:11 +08:00
mlus
37218c4c58 fixed #16 2024-05-03 16:14:21 +08:00
mlus
41298da321 fixed #15 2024-05-02 20:17:15 +08:00
mlus
17ad40f693 code refactor 2024-05-02 16:47:08 +08:00
mlus
0dfab35b25 fix curios inventory replicate bug 2024-04-23 21:27:02 +08:00
mlus
50648a217d fix curios inventory missing bug 2024-04-20 21:19:19 +08:00
mlus
d02232ca2d update curios API X2 2024-04-17 21:57:53 +08:00
mlus
1314911e74
Update build-1.20.yml 2024-04-16 23:51:31 +08:00
mlus
acfb377cee update curios API 2024-04-16 23:50:29 +08:00
mlus
6e6326bbbc trying to fix sql issue 2024-02-19 17:17:32 +08:00
mlus
b3352fde51 1.3.0 update 2024-02-11 17:34:19 +08:00
mlus-Asuka
da99e59d0a wtf 2023-09-29 17:07:25 +08:00
mlus-Asuka
d32ae52537 update 1.2.1 2023-09-29 16:50:26 +08:00
mlus-Asuka
5de0fdcff6 update 1.20.1 2023-09-22 12:07:58 +08:00
mlus-Asuka
45e13f1199 1.2.0 release 2023-08-09 16:08:23 +08:00
mlus-Asuka
5ef817a9b7 Use HikariPool instead of original driver.Merge Fork from KK1ve. 2023-08-03 22:05:43 +08:00
mlus-Asuka
a0201cbb66 Merge remote-tracking branch 'origin/master' 2023-07-05 15:40:55 +08:00
Roderick Upton
65c964bed2
Update README.md 2023-06-27 23:17:19 +08:00
Roderick Upton
8cb31445ef
Update README.md 2023-06-27 23:16:53 +08:00
mlus-Asuka
93404dcdb5 change wrong file path 2023-06-20 15:55:04 +08:00
mlus-Asuka
a9f7c8e361 Merge remote-tracking branch 'origin/master'
# Conflicts:
#	src/main/java/vip/fubuki/playersync/sync/VanillaSync.java
2023-04-06 16:50:09 +08:00
mlus-Asuka
2496383c71 debug 2023-04-06 16:49:48 +08:00
mlus-Asuka
91add1b627 add datapack support. 2023-03-31 19:13:06 +08:00
mlus-Asuka
829406b57a add datapack support. 2023-03-31 17:22:26 +08:00
mlus-Asuka
8940c8be8d fix curios bugs. 2023-03-31 13:18:36 +08:00
mlus-Asuka
dbe90ecd25 fix armor bugs. 2023-03-31 12:11:35 +08:00
mlus-Asuka
30b1690a73 fix chat and effects bugs. 2023-02-16 14:26:24 +08:00
mlus-Asuka
d2921adc88 change data type. 2023-02-16 13:00:18 +08:00
mlus-Asuka
029789ccd1 fix server bug. 2023-02-15 16:06:54 +08:00
38 changed files with 3178 additions and 661 deletions

11
.github/dependabot.yml vendored Normal file
View File

@ -0,0 +1,11 @@
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/" # Location of package manifests
schedule:
interval: "monthly"

39
.github/workflows/backport-prs.yml vendored Normal file
View File

@ -0,0 +1,39 @@
name: Backport merged pull request
on:
pull_request_target:
types: [closed]
issue_comment:
types: [created]
permissions:
contents: write # so it can comment
pull-requests: write # so it can create pull requests
jobs:
backport:
name: Backport pull request
runs-on: ubuntu-latest
# Only run when pull request is merged
# or when a comment starting with `/backport` is created by someone other than the
# https://github.com/backport-action bot user (user id: 97796249). Note that if you use your
# own PAT as `github_token`, that you should replace this id with yours.
if: >
(
github.event_name == 'pull_request_target' &&
github.event.pull_request.merged
) || (
github.event_name == 'issue_comment' &&
github.event.issue.pull_request &&
github.event.comment.user.id != 97796249 &&
startsWith(github.event.comment.body, '/backport')
)
steps:
- uses: actions/checkout@v6
- name: Create backport pull requests
uses: korthout/backport-action@v4
with:
github_token: ${{ secrets.TOKEN }}
pull_description: |
Backport of #${pull_number} to `${target_branch}`.
### Description
${pull_description}

53
.github/workflows/build.yml vendored Normal file
View File

@ -0,0 +1,53 @@
name: Build
on:
pull_request:
push:
branches:
- '1.**'
tags:
- '**'
jobs:
build:
permissions:
contents: write
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@v6
- name: Validate Gradle Wrapper
uses: gradle/actions/wrapper-validation@v6
- name: Set up JDK 17
uses: actions/setup-java@v5
with:
java-version: '17'
distribution: 'temurin'
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v6
- name: Build with Gradle
run: ./gradlew build
- name: Build Artifact
uses: actions/upload-artifact@v7
with:
name: Player_Sync
path: |
build/libs/
- uses: Kir-Antipov/mc-publish@v3.3
# run only on tags, no other pushes
if: startsWith( github.ref, 'refs/tags' )
with:
modrinth-id: 4pmkajBP
modrinth-token: ${{ secrets.MODRINTH_TOKEN }}
curseforge-id: 737274
curseforge-token: ${{ secrets.CURSEFORGE_TOKEN }}
github-token: ${{ secrets.GITHUB_TOKEN }}

11
.gitignore vendored
View File

@ -13,6 +13,17 @@ out
*.iml
.idea
### VisualStudioCode ###
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
# !.vscode/launch.json # launch is customly generated by gradlew
!.vscode/extensions.json
!.vscode/*.code-snippets
# Local History for Visual Studio Code
.history/
# gradle
build
.gradle

61
.vscode/launch.json vendored
View File

@ -1,61 +0,0 @@
{
"version": "0.2.0",
"configurations": [
{
"type": "java",
"name": "runClient",
"request": "launch",
"mainClass": "cpw.mods.bootstraplauncher.BootstrapLauncher",
"projectName": "build",
"cwd": "${workspaceFolder}\\run",
"vmArgs": "-Dforge.logging.console.level\u003ddebug -Dforge.logging.markers\u003dREGISTRIES -DlegacyClassPath.file\u003dD:\\a\\forge-mdk-offline\\forge-mdk-offline\\build\\build\\classpath\\runClient_minecraftClasspath.txt -DignoreList\u003dbootstraplauncher,securejarhandler,asm-commons,asm-util,asm-analysis,asm-tree,asm,JarJarFileSystems,client-extra,fmlcore,javafmllanguage,lowcodelanguage,mclanguage,forge- -DmergeModules\u003djna-5.10.0.jar,jna-platform-5.10.0.jar -Dforge.enabledGameTestNamespaces\u003dexamplemod -Dforge.enableGameTest\u003dtrue -Djava.net.preferIPv6Addresses\u003dsystem -p C:\\Users\\runneradmin\\.gradle\\caches\\modules-2\\files-2.1\\cpw.mods\\bootstraplauncher\\1.1.2\\c546e00443d8432cda6baa1c860346980742628\\bootstraplauncher-1.1.2.jar;C:\\Users\\runneradmin\\.gradle\\caches\\modules-2\\files-2.1\\cpw.mods\\securejarhandler\\2.1.4\\f47e3b9dd860a7b82154b8f90a650ffd0aaa5582\\securejarhandler-2.1.4.jar;C:\\Users\\runneradmin\\.gradle\\caches\\modules-2\\files-2.1\\org.ow2.asm\\asm-commons\\9.3\\1f2a432d1212f5c352ae607d7b61dcae20c20af5\\asm-commons-9.3.jar;C:\\Users\\runneradmin\\.gradle\\caches\\modules-2\\files-2.1\\org.ow2.asm\\asm-util\\9.3\\9595bc05510d0bd4b610188b77333fe4851a1975\\asm-util-9.3.jar;C:\\Users\\runneradmin\\.gradle\\caches\\modules-2\\files-2.1\\org.ow2.asm\\asm-analysis\\9.3\\4b071f211b37c38e0e9f5998550197c8593f6ad8\\asm-analysis-9.3.jar;C:\\Users\\runneradmin\\.gradle\\caches\\modules-2\\files-2.1\\org.ow2.asm\\asm-tree\\9.3\\78d2ecd61318b5a58cd04fb237636c0e86b77d97\\asm-tree-9.3.jar;C:\\Users\\runneradmin\\.gradle\\caches\\modules-2\\files-2.1\\org.ow2.asm\\asm\\9.3\\8e6300ef51c1d801a7ed62d07cd221aca3a90640\\asm-9.3.jar;C:\\Users\\runneradmin\\.gradle\\caches\\modules-2\\files-2.1\\net.minecraftforge\\JarJarFileSystems\\0.3.16\\e52afbb2599dc7c6f779efea0496d32fc01152e3\\JarJarFileSystems-0.3.16.jar --add-modules ALL-MODULE-PATH --add-opens java.base/java.util.jar\u003dcpw.mods.securejarhandler --add-opens java.base/java.lang.invoke\u003dcpw.mods.securejarhandler --add-exports java.base/sun.security.util\u003dcpw.mods.securejarhandler --add-exports jdk.naming.dns/com.sun.jndi.dns\u003djava.naming -XX:HeapDumpPath\u003dMojangTricksIntelDriversForPerformance_javaw.exe_minecraft.exe.heapdump \"-Dos.name\u003dWindows 10\" -Dos.version\u003d10.0",
"args": "--launchTarget forgeclientuserdev --version MOD_DEV --assetIndex 1.19 --assetsDir C:\\Users\\runneradmin\\.gradle\\caches\\forge_gradle\\assets --gameDir . --fml.forgeVersion 43.1.1 --fml.mcVersion 1.19.2 --fml.forgeGroup net.minecraftforge --fml.mcpVersion 20220805.130853",
"env": {
"MOD_CLASSES": "examplemod%%${workspaceFolder}\\bin\\main;examplemod%%${workspaceFolder}\\bin\\main",
"MCP_MAPPINGS": "official_1.19.2"
}
},
{
"type": "java",
"name": "runData",
"request": "launch",
"mainClass": "cpw.mods.bootstraplauncher.BootstrapLauncher",
"projectName": "build",
"cwd": "${workspaceFolder}\\run",
"vmArgs": "-Dforge.logging.console.level\u003ddebug -Dforge.logging.markers\u003dREGISTRIES -DlegacyClassPath.file\u003dD:\\a\\forge-mdk-offline\\forge-mdk-offline\\build\\build\\classpath\\runData_minecraftClasspath.txt -DignoreList\u003dbootstraplauncher,securejarhandler,asm-commons,asm-util,asm-analysis,asm-tree,asm,JarJarFileSystems,client-extra,fmlcore,javafmllanguage,lowcodelanguage,mclanguage,forge- -DmergeModules\u003djna-5.10.0.jar,jna-platform-5.10.0.jar -Djava.net.preferIPv6Addresses\u003dsystem -p C:\\Users\\runneradmin\\.gradle\\caches\\modules-2\\files-2.1\\cpw.mods\\bootstraplauncher\\1.1.2\\c546e00443d8432cda6baa1c860346980742628\\bootstraplauncher-1.1.2.jar;C:\\Users\\runneradmin\\.gradle\\caches\\modules-2\\files-2.1\\cpw.mods\\securejarhandler\\2.1.4\\f47e3b9dd860a7b82154b8f90a650ffd0aaa5582\\securejarhandler-2.1.4.jar;C:\\Users\\runneradmin\\.gradle\\caches\\modules-2\\files-2.1\\org.ow2.asm\\asm-commons\\9.3\\1f2a432d1212f5c352ae607d7b61dcae20c20af5\\asm-commons-9.3.jar;C:\\Users\\runneradmin\\.gradle\\caches\\modules-2\\files-2.1\\org.ow2.asm\\asm-util\\9.3\\9595bc05510d0bd4b610188b77333fe4851a1975\\asm-util-9.3.jar;C:\\Users\\runneradmin\\.gradle\\caches\\modules-2\\files-2.1\\org.ow2.asm\\asm-analysis\\9.3\\4b071f211b37c38e0e9f5998550197c8593f6ad8\\asm-analysis-9.3.jar;C:\\Users\\runneradmin\\.gradle\\caches\\modules-2\\files-2.1\\org.ow2.asm\\asm-tree\\9.3\\78d2ecd61318b5a58cd04fb237636c0e86b77d97\\asm-tree-9.3.jar;C:\\Users\\runneradmin\\.gradle\\caches\\modules-2\\files-2.1\\org.ow2.asm\\asm\\9.3\\8e6300ef51c1d801a7ed62d07cd221aca3a90640\\asm-9.3.jar;C:\\Users\\runneradmin\\.gradle\\caches\\modules-2\\files-2.1\\net.minecraftforge\\JarJarFileSystems\\0.3.16\\e52afbb2599dc7c6f779efea0496d32fc01152e3\\JarJarFileSystems-0.3.16.jar --add-modules ALL-MODULE-PATH --add-opens java.base/java.util.jar\u003dcpw.mods.securejarhandler --add-opens java.base/java.lang.invoke\u003dcpw.mods.securejarhandler --add-exports java.base/sun.security.util\u003dcpw.mods.securejarhandler --add-exports jdk.naming.dns/com.sun.jndi.dns\u003djava.naming",
"args": "--launchTarget forgedatauserdev --assetIndex 1.19 --assetsDir C:\\Users\\runneradmin\\.gradle\\caches\\forge_gradle\\assets --gameDir . --fml.forgeVersion 43.1.1 --fml.mcVersion 1.19.2 --fml.forgeGroup net.minecraftforge --fml.mcpVersion 20220805.130853 --mod examplemod --all --output D:\\a\\forge-mdk-offline\\forge-mdk-offline\\build\\src\\generated\\resources --existing D:\\a\\forge-mdk-offline\\forge-mdk-offline\\build\\src\\main\\resources",
"env": {
"MOD_CLASSES": "examplemod%%${workspaceFolder}\\bin\\main;examplemod%%${workspaceFolder}\\bin\\main",
"MCP_MAPPINGS": "official_1.19.2"
}
},
{
"type": "java",
"name": "runGameTestServer",
"request": "launch",
"mainClass": "cpw.mods.bootstraplauncher.BootstrapLauncher",
"projectName": "build",
"cwd": "${workspaceFolder}\\run",
"vmArgs": "-Dforge.logging.console.level\u003ddebug -Dforge.logging.markers\u003dREGISTRIES -DlegacyClassPath.file\u003dD:\\a\\forge-mdk-offline\\forge-mdk-offline\\build\\build\\classpath\\runGameTestServer_minecraftClasspath.txt -DignoreList\u003dbootstraplauncher,securejarhandler,asm-commons,asm-util,asm-analysis,asm-tree,asm,JarJarFileSystems,client-extra,fmlcore,javafmllanguage,lowcodelanguage,mclanguage,forge- -DmergeModules\u003djna-5.10.0.jar,jna-platform-5.10.0.jar -Dforge.enabledGameTestNamespaces\u003dexamplemod -Djava.net.preferIPv6Addresses\u003dsystem -p C:\\Users\\runneradmin\\.gradle\\caches\\modules-2\\files-2.1\\cpw.mods\\bootstraplauncher\\1.1.2\\c546e00443d8432cda6baa1c860346980742628\\bootstraplauncher-1.1.2.jar;C:\\Users\\runneradmin\\.gradle\\caches\\modules-2\\files-2.1\\cpw.mods\\securejarhandler\\2.1.4\\f47e3b9dd860a7b82154b8f90a650ffd0aaa5582\\securejarhandler-2.1.4.jar;C:\\Users\\runneradmin\\.gradle\\caches\\modules-2\\files-2.1\\org.ow2.asm\\asm-commons\\9.3\\1f2a432d1212f5c352ae607d7b61dcae20c20af5\\asm-commons-9.3.jar;C:\\Users\\runneradmin\\.gradle\\caches\\modules-2\\files-2.1\\org.ow2.asm\\asm-util\\9.3\\9595bc05510d0bd4b610188b77333fe4851a1975\\asm-util-9.3.jar;C:\\Users\\runneradmin\\.gradle\\caches\\modules-2\\files-2.1\\org.ow2.asm\\asm-analysis\\9.3\\4b071f211b37c38e0e9f5998550197c8593f6ad8\\asm-analysis-9.3.jar;C:\\Users\\runneradmin\\.gradle\\caches\\modules-2\\files-2.1\\org.ow2.asm\\asm-tree\\9.3\\78d2ecd61318b5a58cd04fb237636c0e86b77d97\\asm-tree-9.3.jar;C:\\Users\\runneradmin\\.gradle\\caches\\modules-2\\files-2.1\\org.ow2.asm\\asm\\9.3\\8e6300ef51c1d801a7ed62d07cd221aca3a90640\\asm-9.3.jar;C:\\Users\\runneradmin\\.gradle\\caches\\modules-2\\files-2.1\\net.minecraftforge\\JarJarFileSystems\\0.3.16\\e52afbb2599dc7c6f779efea0496d32fc01152e3\\JarJarFileSystems-0.3.16.jar --add-modules ALL-MODULE-PATH --add-opens java.base/java.util.jar\u003dcpw.mods.securejarhandler --add-opens java.base/java.lang.invoke\u003dcpw.mods.securejarhandler --add-exports java.base/sun.security.util\u003dcpw.mods.securejarhandler --add-exports jdk.naming.dns/com.sun.jndi.dns\u003djava.naming",
"args": "--launchTarget forgegametestserveruserdev --gameDir . --fml.forgeVersion 43.1.1 --fml.mcVersion 1.19.2 --fml.forgeGroup net.minecraftforge --fml.mcpVersion 20220805.130853",
"env": {
"MOD_CLASSES": "examplemod%%${workspaceFolder}\\bin\\main;examplemod%%${workspaceFolder}\\bin\\main",
"MCP_MAPPINGS": "official_1.19.2"
}
},
{
"type": "java",
"name": "runServer",
"request": "launch",
"mainClass": "cpw.mods.bootstraplauncher.BootstrapLauncher",
"projectName": "build",
"cwd": "${workspaceFolder}\\run",
"vmArgs": "-Dforge.logging.console.level\u003ddebug -Dforge.logging.markers\u003dREGISTRIES -DlegacyClassPath.file\u003dD:\\a\\forge-mdk-offline\\forge-mdk-offline\\build\\build\\classpath\\runServer_minecraftClasspath.txt -DignoreList\u003dbootstraplauncher,securejarhandler,asm-commons,asm-util,asm-analysis,asm-tree,asm,JarJarFileSystems,client-extra,fmlcore,javafmllanguage,lowcodelanguage,mclanguage,forge- -DmergeModules\u003djna-5.10.0.jar,jna-platform-5.10.0.jar -Dforge.enabledGameTestNamespaces\u003dexamplemod -Dforge.enableGameTest\u003dtrue -Djava.net.preferIPv6Addresses\u003dsystem -p C:\\Users\\runneradmin\\.gradle\\caches\\modules-2\\files-2.1\\cpw.mods\\bootstraplauncher\\1.1.2\\c546e00443d8432cda6baa1c860346980742628\\bootstraplauncher-1.1.2.jar;C:\\Users\\runneradmin\\.gradle\\caches\\modules-2\\files-2.1\\cpw.mods\\securejarhandler\\2.1.4\\f47e3b9dd860a7b82154b8f90a650ffd0aaa5582\\securejarhandler-2.1.4.jar;C:\\Users\\runneradmin\\.gradle\\caches\\modules-2\\files-2.1\\org.ow2.asm\\asm-commons\\9.3\\1f2a432d1212f5c352ae607d7b61dcae20c20af5\\asm-commons-9.3.jar;C:\\Users\\runneradmin\\.gradle\\caches\\modules-2\\files-2.1\\org.ow2.asm\\asm-util\\9.3\\9595bc05510d0bd4b610188b77333fe4851a1975\\asm-util-9.3.jar;C:\\Users\\runneradmin\\.gradle\\caches\\modules-2\\files-2.1\\org.ow2.asm\\asm-analysis\\9.3\\4b071f211b37c38e0e9f5998550197c8593f6ad8\\asm-analysis-9.3.jar;C:\\Users\\runneradmin\\.gradle\\caches\\modules-2\\files-2.1\\org.ow2.asm\\asm-tree\\9.3\\78d2ecd61318b5a58cd04fb237636c0e86b77d97\\asm-tree-9.3.jar;C:\\Users\\runneradmin\\.gradle\\caches\\modules-2\\files-2.1\\org.ow2.asm\\asm\\9.3\\8e6300ef51c1d801a7ed62d07cd221aca3a90640\\asm-9.3.jar;C:\\Users\\runneradmin\\.gradle\\caches\\modules-2\\files-2.1\\net.minecraftforge\\JarJarFileSystems\\0.3.16\\e52afbb2599dc7c6f779efea0496d32fc01152e3\\JarJarFileSystems-0.3.16.jar --add-modules ALL-MODULE-PATH --add-opens java.base/java.util.jar\u003dcpw.mods.securejarhandler --add-opens java.base/java.lang.invoke\u003dcpw.mods.securejarhandler --add-exports java.base/sun.security.util\u003dcpw.mods.securejarhandler --add-exports jdk.naming.dns/com.sun.jndi.dns\u003djava.naming",
"args": "--launchTarget forgeserveruserdev --gameDir . --fml.forgeVersion 43.1.1 --fml.mcVersion 1.19.2 --fml.forgeGroup net.minecraftforge --fml.mcpVersion 20220805.130853",
"env": {
"MOD_CLASSES": "examplemod%%${workspaceFolder}\\bin\\main;examplemod%%${workspaceFolder}\\bin\\main",
"MCP_MAPPINGS": "official_1.19.2"
}
}
]
}

10
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,10 @@
{
"java.compile.nullAnalysis.mode": "automatic",
"java.completion.importOrder": [
"",
"javax",
"java",
"#"
],
"java.sources.organizeImports.starThreshold": 5
}

View File

@ -1,7 +1,63 @@
# PlayerSync
This is a Minecraft forge mod using Mysql backend to make player data synchronization between different servers.
Such as equipment,inventory,effects,experience,food level.Any other mods support is also possible.
Support version now:
1.19
Current support Mod:
curios
PlayerSync is a Minecraft Forge mod that synchronizes player data across multiple servers using a MySQL backend. It allows players to maintain their inventory, equipment, experience, advancements, and more when moving between servers in a network.
## Mod Support
* [Curios API](https://www.curseforge.com/minecraft/mc-mods/curios)
* [Sophisticated Backpacks](https://www.curseforge.com/minecraft/mc-mods/sophisticated-backpacks)
Any other mods support is also possible.
## Development Setup
### Database Setup (Docker)
A `docker-compose.yml` file is provided for easily setting up a MariaDB database instance for development testing.
1. Make sure Docker is installed.
1. Inside your work directory run:
```sh
docker compose up -d
```
This will download the MariaDB image (if not already present) and start a database container in the background.
1. Stoppinng the Database
```sh
docker compose down
```
**Data Persistence:** The database uses a Docker volume, ensuring your data persists even if you stop and restart the containers.
#### Database Management Tool
The `docker-compose.yml` also includes an [Adminer](https://www.adminer.org/) service, a lightweight database management tool.
* Access Adminer in your web browser at http://localhost:8080.
* Log in using the server with
- username: `playersync`
- database: `playersync`
- password: see [docker-compose.yml](./docker-compose.yml)
For debugging purposes, you can enable `use_legacy_serialization` to have readable database fields. This can cause crashes and unintended side-effects. **Do not enable this on a production server if not absolutely necessary!**
### Running the Mod
The project uses Gradle for building and running. Use the provided Gradle wrapper (`gradlew` for Linux/macOS, `gradlew.bat` for Windows).
1. Make sure that the MySQL database you configured is running.
1. Run the Server
```sh
./gradlew runServer
```
or on Windows:
```bat
.\gradlew.bat runServer
```
This task compiles the mod and starts a dedicated Minecraft server instance with the mod loaded in the `run` directory.
1. Run the Client
```sh
./gradlew runClient
```
or on Windows:
```bat
.\gradlew.bat runClient
```

View File

@ -1,115 +1,113 @@
plugins {
id 'eclipse'
id 'idea'
id 'java-library'
id 'maven-publish'
id 'net.minecraftforge.gradle' version '5.1.+'
id 'net.neoforged.moddev.legacyforge' version '2.0.84'
}
version = '1.0'
group = 'vip.fubuki.PlayerSync' // http://maven.apache.org/guides/mini/guide-naming-conventions.html
archivesBaseName = 'playersync'
tasks.named('wrapper', Wrapper).configure {
// Define wrapper values here so as to not have to always do so when updating gradlew.properties.
// Switching this to Wrapper.DistributionType.ALL will download the full gradle sources that comes with
// documentation attached on cursor hover of gradle classes and methods. However, this comes with increased
// file size for Gradle. If you do switch this to ALL, run the Gradle wrapper task twice afterwards.
// (Verify by checking gradle/wrapper/gradle-wrapper.properties to see if distributionUrl now points to `-all`)
distributionType = Wrapper.DistributionType.BIN
}
version = minecraft_version + "-" + mod_version
group = mod_group_id
repositories {
mavenLocal()
exclusiveContent {
forRepository {
maven {
url "https://cursemaven.com"
}
}
filter {
includeGroup "curse.maven"
}
}
maven { url 'https://modmaven.dev/' }
}
base {
archivesName = mod_id
}
// Mojang ships Java 17 to end users in 1.20.1, so mods should target Java 17.
java.toolchain.languageVersion = JavaLanguageVersion.of(17)
println "Java: ${System.getProperty 'java.version'}, JVM: ${System.getProperty 'java.vm.version'} (${System.getProperty 'java.vendor'}), Arch: ${System.getProperty 'os.arch'}"
minecraft {
// The mappings can be changed at any time and must be in the following format.
// Channel: Version:
// official MCVersion Official field/method names from Mojang mapping files
// parchment YYYY.MM.DD-MCVersion Open community-sourced parameter names and javadocs layered on top of official
//
// You must be aware of the Mojang license when using the 'official' or 'parchment' mappings.
// See more information here: https://github.com/MinecraftForge/MCPConfig/blob/master/Mojang.md
//
// Parchment is an unofficial project maintained by ParchmentMC, separate from MinecraftForge
// Additional setup is needed to use their mappings: https://github.com/ParchmentMC/Parchment/wiki/Getting-Started
//
// Use non-default mappings at your own risk. They may not always work.
// Simply re-run your setup task after changing the mappings to update your workspace.
mappings channel: 'official', version: '1.19.2'
legacyForge {
// Specify the version of MinecraftForge to use.
version = project.minecraft_version + '-' + project.forge_version
//accessTransformer = file('src/main/resources/META-INF/accesstransformer.cfg') // Currently, this location cannot be changed from the default.
parchment {
mappingsVersion = project.parchment_mappings_version
minecraftVersion = project.parchment_minecraft_version
}
// This line is optional. Access Transformers are automatically detected
// accessTransformers = project.files('src/main/resources/META-INF/accesstransformer.cfg')
// Default run configurations.
// These can be tweaked, removed, or duplicated as needed.
runs {
client {
workingDirectory project.file('run')
// Recommended logging data for a userdev environment
// The markers can be added/remove as needed separated by commas.
// "SCAN": For mods scan.
// "REGISTRIES": For firing of registry events.
// "REGISTRYDUMP": For getting the contents of all registries.
property 'forge.logging.markers', 'REGISTRIES'
// Recommended logging level for the console
// You can set various levels here.
// Please read: https://stackoverflow.com/questions/2031163/when-to-use-the-different-log-levels
property 'forge.logging.console.level', 'debug'
client()
// Comma-separated list of namespaces to load gametests from. Empty = all namespaces.
property 'forge.enabledGameTestNamespaces', 'playersync'
mods {
playsersync {
source sourceSets.main
}
}
systemProperty 'forge.enabledGameTestNamespaces', project.mod_id
}
server {
workingDirectory project.file('run')
property 'forge.logging.markers', 'REGISTRIES'
property 'forge.logging.console.level', 'debug'
property 'forge.enabledGameTestNamespaces', 'playersync'
mods {
playersync {
source sourceSets.main
}
}
server()
programArgument '--nogui'
systemProperty 'forge.enabledGameTestNamespaces', project.mod_id
}
// This run config launches GameTestServer and runs all registered gametests, then exits.
// By default, the server will crash when no gametests are provided.
// The gametest system is also enabled by default for other run configs under the /test command.
gameTestServer {
workingDirectory project.file('run')
property 'forge.logging.markers', 'REGISTRIES'
property 'forge.logging.console.level', 'debug'
property 'forge.enabledGameTestNamespaces', 'playersync'
property 'mixin.env.remapRefMap', 'true'
property 'mixin.env.refMapRemappingFile', "${buildDir}/createSrgToMcp/output.srg"
mods {
playersync {
source sourceSets.main
}
}
type = "gameTestServer"
systemProperty 'forge.enabledGameTestNamespaces', project.mod_id
}
data {
workingDirectory project.file('run')
data()
property 'forge.logging.markers', 'REGISTRIES'
property 'forge.logging.console.level', 'debug'
// example of overriding the workingDirectory set in configureEach above, uncomment if you want to use it
// gameDirectory = project.file('run-data')
// Specify the modid for data generation, where to output the resulting resource, and where to look for existing resources.
args '--mod', 'playersync', '--all', '--output', file('src/generated/resources/'), '--existing', file('src/main/resources/')
programArguments.addAll '--mod', project.mod_id, '--all', '--output', file('src/generated/resources/').getAbsolutePath(), '--existing', file('src/main/resources/').getAbsolutePath()
}
mods {
playersync{
source sourceSets.main
}
}
// applies to all the run configs above
configureEach {
// Recommended logging data for a userdev environment
// The markers can be added/remove as needed separated by commas.
// "SCAN": For mods scan.
// "REGISTRIES": For firing of registry events.
// "REGISTRYDUMP": For getting the contents of all registries.
systemProperty 'forge.logging.markers', 'REGISTRIES'
// Recommended logging level for the console
// You can set various levels here.
// Please read: https://stackoverflow.com/questions/2031163/when-to-use-the-different-log-levels
logLevel = org.slf4j.event.Level.DEBUG
}
}
mods {
// define mod <-> source bindings
// these are used to tell the game which sources are for which mod
// mostly optional in a single mod project
// but multi mod projects should define one per mod
"${mod_id}" {
sourceSet(sourceSets.main)
}
}
}
@ -117,62 +115,117 @@ minecraft {
// Include resources generated by data generators.
sourceSets.main.resources { srcDir 'src/generated/resources' }
repositories {
maven {
url = "https://maven.theillusivec4.top/"
}
// Put repositories for dependencies here
// ForgeGradle automatically adds the Forge maven and Maven Central for you
// If you have mod jar dependencies in ./libs, you can declare them as a repository like so:
// flatDir {
// dir 'libs'
// }
// Sets up a dependency configuration called 'localRuntime' and a deobfuscating one called 'modLocalRuntime'
// These configurations should be used instead of 'runtimeOnly' to declare
// a dependency that will be present for runtime testing but that is
// "optional", meaning it will not be pulled by dependents of this mod.
configurations {
runtimeClasspath.extendsFrom localRuntime
}
obfuscation {
createRemappingConfiguration(configurations.localRuntime)
}
dependencies {
minecraft 'net.minecraftforge:forge:1.19.2-43.1.1'
runtimeOnly fg.deobf("top.theillusivec4.curios:curios-forge:1.19.2-5.1.1.0")
compileOnly fg.deobf("top.theillusivec4.curios:curios-forge:1.19.2-5.1.1.0:api")
// compileOnly fg.deobf("mezz.jei:jei-${mc_version}:${jei_version}:api") // Adds JEI API as a compile dependency
// runtimeOnly fg.deobf("mezz.jei:jei-${mc_version}:${jei_version}") // Adds the full JEI mod as a runtime dependency
// implementation fg.deobf("com.tterrag.registrate:Registrate:MC${mc_version}-${registrate_version}") // Adds registrate as a dependency
// If you wish to declare dependencies against mods, make sure to use the 'mod*' configurations so that they're remapped.
// See https://github.com/neoforged/ModDevGradle/blob/main/LEGACY.md#remapping-mod-dependencies for more information.
// Examples using mod jars from ./libs
implementation fileTree(dir:'/libs',include:['*.jar'])
// Example optional mod dependency with JEI
// The JEI API is declared for compile time use, while the full JEI artifact is used at runtime
// modCompileOnly "mezz.jei:jei-${mc_version}-common-api:${jei_version}"
// modCompileOnly "mezz.jei:jei-${mc_version}-neoforge-api:${jei_version}"
// We add the full version to localRuntime, not runtimeOnly, so that we do not publish a dependency on it
// modLocalRuntime "mezz.jei:jei-${mc_version}-neoforge:${jei_version}"
compileOnly "curse.maven:curios-309927:5266541"
compileOnly "curse.maven:sophisticated-backpacks-422301:7169843"
compileOnly "curse.maven:sophisticated-core-618298:7169400"
// compileOnly "mekanism:Mekanism:${mekanism_version}:api"
// If you want to test/use Mekanism & its modules during `runClient` invocation, use the following
modCompileOnly ("mekanism:Mekanism:${mekanism_version}")// core
// modcompileOnly ("mekanism:Mekanism:${mekanism_version}:additions")// Mekanism: Additions
// modcompileOnly ("mekanism:Mekanism:${mekanism_version}:generators")// Mekanism: Generators
// modcompileOnly ("mekanism:Mekanism:${mekanism_version}:tools")// Mekanism: Tools
// Example mod dependency using a mod jar from ./libs with a flat dir repository
// This maps to ./libs/coolmod-${mc_version}-${coolmod_version}.jar
// The group id is ignored when searching -- in this case, it is "blank"
// modImplementation "blank:coolmod-${mc_version}:${coolmod_version}"
// Example mod dependency using a file as dependency
// modImplementation files("libs/coolmod-${mc_version}-${coolmod_version}.jar")
// Example project dependency using a sister or child project:
// modImplementation project(":myproject")
// embedd the JDBC driver in the mod using jarJar
// JDBC driver auto-detection is broken in Forge as of v47.4.0
// i.e. we need to need it both at compile and runtime
implementation "com.mysql:mysql-connector-j:${jdbc_version}"
jarJar "com.mysql:mysql-connector-j:${jdbc_version}"
additionalRuntimeClasspath "com.mysql:mysql-connector-j:${jdbc_version}"
// For more info:
// http://www.gradle.org/docs/current/userguide/artifact_dependencies_tutorial.html
// http://www.gradle.org/docs/current/userguide/dependency_management.html
}
// Uncomment the lines below if you wish to configure mixin. The mixin file should be named modid.mixins.json.
/*
mixin {
add sourceSets.main, "${mod_id}.refmap.json"
config "${mod_id}.mixins.json"
}
dependencies {
annotationProcessor 'org.spongepowered:mixin:0.8.5:processor'
}
// Example for how to get properties into the manifest for reading at runtime.
jar {
manifest {
attributes([
"Specification-Title" : "playersync",
"Specification-Vendor" : "examplemodsareus",
"Specification-Version" : "1", // We are version 1 of ourselves
"Implementation-Title" : project.name,
"Implementation-Version" : project.jar.archiveVersion,
"Implementation-Vendor" : "examplemodsareus",
"Implementation-Timestamp": new Date().format("yyyy-MM-dd'T'HH:mm:ssZ")
])
}
manifest.attributes([
"MixinConfigs": "${mod_id}.mixins.json"
])
}
*/
// This block of code expands all declared replace properties in the specified resource targets.
// A missing property will result in an error. Properties are expanded using ${} Groovy notation.
var generateModMetadata = tasks.register("generateModMetadata", ProcessResources) {
var replaceProperties = [
minecraft_version : minecraft_version,
minecraft_version_range : minecraft_version_range,
forge_version : forge_version,
forge_version_range : forge_version_range,
loader_version_range : loader_version_range,
mod_id : mod_id,
mod_name : mod_name,
mod_license : mod_license,
mod_version : mod_version,
mod_authors : mod_authors,
mod_description : mod_description
]
inputs.properties replaceProperties
expand replaceProperties
from "src/main/templates"
into "build/generated/sources/modMetadata"
}
// Include the output of "generateModMetadata" as an input directory for the build
// this works with both building through Gradle and the IDE.
sourceSets.main.resources.srcDir generateModMetadata
// To avoid having to run "generateModMetadata" manually, make it run on every project reload
legacyForge.ideSyncTask generateModMetadata
// Example configuration to allow publishing using the maven-publish plugin
// This is the preferred method to reobfuscate your jar file
jar.finalizedBy('reobfJar')
// However if you are in a multi-project build, dev time needs unobfed jar files, so you can delay the obfuscation until publishing by doing
// publish.dependsOn('reobfJar')
publishing {
publications {
mavenJava(MavenPublication) {
artifact jar
register('mavenJava', MavenPublication) {
from components.java
}
}
repositories {
maven {
url "file://${project.projectDir}/mcmodsrepo"
url "file://${project.projectDir}/repo"
}
}
}
@ -180,3 +233,11 @@ publishing {
tasks.withType(JavaCompile).configureEach {
options.encoding = 'UTF-8' // Use the UTF-8 charset for Java compilation
}
// IDEA no longer automatically downloads sources/javadoc jars for dependencies, so we need to explicitly enable the behavior.
idea {
module {
downloadSources = true
downloadJavadoc = true
}
}

22
docker-compose.yml Normal file
View File

@ -0,0 +1,22 @@
services:
db:
image: mariadb
restart: unless-stopped
environment:
MARIADB_DATABASE: playersync
MARIADB_USER: playersync
MARIADB_PASSWORD: pleaseChangeThisPassword # It is strongly recommended to change this password outside of local development
MARIADB_RANDOM_ROOT_PASSWORD: True
ports:
- 3306:3306
volumes:
- dbvolume:/var/lib/mysql
adminer:
image: adminer
restart: unless-stopped
ports:
- 8080:8080
volumes:
dbvolume:

406
docs/code-analysis.md Normal file
View File

@ -0,0 +1,406 @@
# PlayerSync — Code Analysis
## 1. Project Overview
**PlayerSync** is a **Minecraft Forge 1.20.1 server-side mod** that synchronizes player data across multiple Forge servers sharing a centralized MySQL database. It is designed for **Forge 群组服 (Forge group server)** architectures where players can move between different physical sub-servers and their inventory, equipment, advancements, effects, and other data must follow them seamlessly.
| Property | Value |
|---|---|
| Mod ID | `playersync` |
| Minecraft | 1.20.1 |
| Forge | 47.4.0+ |
| Java | 17 |
| License | GPL-3.0 |
| Author | mlus |
| DB | MySQL (via JDBC `mysql-connector-j`) |
---
## 2. Project File Structure
```
src/main/java/vip/fubuki/playersync/
├── PlayerSync.java # Main mod class, DB initialization
├── CommandInit.java # `/playersync` command registration
├── config/
│ └── JdbcConfig.java # All mod configuration (ForgeConfigSpec)
├── sync/
│ ├── VanillaSync.java # Core player data sync (inventory, effects, advancements, etc.)
│ ├── ChatSync.java # Cross-server chat sync orchestrator
│ ├── chat/
│ │ ├── ChatSyncServer.java # TCP chat server (one designated server)
│ │ └── ChatSyncClient.java # TCP chat client (all servers)
│ └── addons/
│ ├── ModsSupport.java # Curios, Sophisticated Backpacks & Mekanism integration
│ ├── CuriosCache.java # Death-safe Curios data caching
│ └── MekanismSupport.java # Mekanism personal chest inventory sync
└── util/
├── JDBCsetUp.java # MySQL JDBC connection & query helper
├── LocalJsonUtil.java # Lightweight map⇔string parser
└── PSThreadPoolFactory.java # Named thread factory for async DB operations
src/main/resources/
└── assets/playersync/lang/
├── en_us.json # English translations
└── zh_cn.json # Chinese translations
src/main/templates/
├── META-INF/mods.toml # Mod metadata template
└── pack.mcmeta # Resource pack metadata
```
---
## 3. Architecture
### 3.1 Deployment Topology
```
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Server A │ │ Server B │ │ Server C │
│ (forge-1) │ │ (forge-2) │ │ (forge-3) │
│ PlayerSync │ │ PlayerSync │ │ PlayerSync │
└──────┬───────┘ └──────┬───────┘ └──────┬───────┘
│ │ │
└──────────────────┼──────────────────┘
┌──────▼──────┐ ┌──────────────┐
│ MySQL DB │◄──────│ Chat Server │
│ playersync │ │ (TCP:7900) │
└─────────────┘ └──────────────┘
```
- **All sub-servers** read/write player data to the same MySQL database.
- **One designated server** runs the chat sync server (TCP). All servers connect to it as clients to broadcast chat messages across servers.
- Each server gets a **unique `server_id`** (randomly generated on first config load).
### 3.2 Data Flow Summary
```
Player Login:
1. PlayerNegotiationEvent → doPlayerConnect() [check if already online on another server]
2. OnDatapackSyncEvent → restore advancements [write JSON to disk]
3. PlayerLoggedInEvent → doPlayerJoin() [restore all data from DB]
└─ ModsSupport.doCuriosRestore()
└─ ModsSupport.doBackPackRestore()
└─ MekanismSupport.restorePersonalChestData()
Player Playing:
4. PlayerEvent.SaveToFile → doPlayerSaveToFile() [periodic save]
5. TickEvent.ServerTick → auto-save every 1200t [~1 min]
6. ServerChatEvent → ChatSyncClient → TCP [chat broadcast]
Player Death (with keepInventory):
6. LivingDeathEvent → CuriosCache.tryStoreCuriosToCache() [snapshot curios]
Player Logout:
7. PlayerLoggedOutEvent → onPlayerLeave() → store() + curios save
└─ if dead: use CuriosCache, else normal save
Server Shutdown:
8. ServerStoppedEvent → mark server disabled in DB
```
---
## 4. Core Module Analysis
### 4.1 `PlayerSync.java` — Main Mod Class
**Path:** `src/main/java/vip/fubuki/playersync/PlayerSync.java`
The entry point and lifecycle controller.
| Method | Trigger | Responsibility |
|---|---|---|
| `PlayerSync()` | Mod construction | Register config, event bus |
| `commonSetup()` | `FMLCommonSetupEvent` | Register JDBC driver, init VanillaSync, conditionally init ChatSync |
| `onServerStarting()` | `ServerStartingEvent` | Create DB, create/alter tables, register server heartbeat, mark players offline |
| `onServerStopping()` | `ServerStoppingEvent` | Shutdown ChatSync |
**Database tables created on server start:**
| Table | Purpose |
|---|---|
| `player_data` | Core player state (inv, armor, enderchest, advancements, effects, XP, health, food, score, left_hand, cursors, online flag, last_server) |
| `server_info` | Server heartbeat (id, enable, data_version, last_update) |
| `curios` | Curios inventory (only if Curios mod loaded) |
| `backpack_data` | Sophisticated Backpacks NBT data (only if mod loaded) |
| `mekanism_personal_chest` | Mekanism personal chest inventory data (only if Mekanism loaded) |
The mod runs **automatic schema migration** — it checks column counts and data types on startup and alters tables as needed.
---
### 4.2 `VanillaSync.java` — Core Sync Engine
**Path:** `src/main/java/vip/fubuki/playersync/sync/VanillaSync.java`
The largest and most critical class. All methods are `static` and registered via `@Mod.EventBusSubscriber`.
#### Thread Pool
```
CORE: 2 threads, MAX: 8 threads, QUEUE: 256 (LinkedBlockingQueue)
Rejection policy: CallerRunsPolicy (backpressure)
```
All DB operations run via `executorService.submit(...)` to avoid blocking the server thread. The bounded thread pool with `CallerRunsPolicy` was a fix for #169 — previously an unbounded `CachedThreadPool` could spawn 25000+ threads under load.
#### Event Handlers
| Event | Method | Behavior |
|---|---|---|
| `OnDatapackSyncEvent` | `onDataPackSyncEvent()` | Restore advancements JSON from DB to disk, then reload |
| `PlayerNegotiationEvent` | `onPlayerConnect()` | Pre-login check: kick if player is already online on another active server (5-minute heartbeat window) |
| `PlayerLoggedInEvent` | `onPlayerJoin()` | Full data restore: health, food, XP, score, inventory, armor, ender chest, effects, left hand, cursor. Returns generic placeholders for unknown modded items. |
| `PlayerEvent.SaveToFile` | `onPlayerSaveToFile()` | Save current state to DB (skipped if player not yet synced) |
| `PlayerLoggedOutEvent` | `onPlayerLogout()` | Mark offline in DB, save data, handle dead/dying edge cases |
| `LivingDeathEvent` | `onPlayerDeath()` | Cache Curios data (for keepInventory worlds) |
| `TickEvent.LevelTick` | `onUpdate()` | Server heartbeat: update `last_update` every 1800 ticks (~90s) |
| `TickEvent.ServerTick` | `onServerTick()` | Auto-save all online players every 1200 ticks (~1 min); clean expired Curios cache every 36000 ticks (~30 min) |
| `ServerStoppedEvent` | `onServerShutdown()` | Mark server as disabled in DB |
#### Serialization
- **New format (default):** Base64 with `B64:` prefix (`B64:<base64-nbt-string>`)
- **Legacy format:** Custom character replacement (`|`, `^`, `<`, `>`, `~` for `,`, `"`, `{`, `}`, `'`)
- The mod **reads both formats** but writes using the configured format (`use_legacy_serialization` config flag)
- NBT is always tagged with the current data version for forward compatibility
#### Placeholder Items
When an item from one server's mod set doesn't exist on another, it becomes a **paper item** with:
- Red italic name: "Item Voucher" (customizable)
- Lore showing the original item ID and stack count
- Stored `playersync:original_item_nbt` tag for later restoration
- Unique UUID to prevent stacking
#### XP Calculation
- `getTotalExperience()` — converts level+progress to absolute XP using Minecraft's level curve formulas (different for levels 0-15, 16-30, 31+)
- `setXpForPlayer()` — inverse: distributes absolute XP into level+progress, bypassing `giveExperience()` side effects (events, packets, score)
#### Dead/Dying Player Edge Case
When a player dies during login (`isDeadOrDying()`), the mod:
1. Clears the `player_synced` tag
2. Teleports to respawn point
3. Sets health to 1
4. Updates online status
5. Disconnects player with a reconnect message
This avoids saving corrupted "dead" state as the player's synced data.
---
### 4.3 `ChatSync.java` — Chat Sync Orchestrator
**Path:** `src/main/java/vip/fubuki/playersync/sync/ChatSync.java`
Starts both the **TCP server** (if configured as chat server) and **TCP client** on separate threads.
- `ChatSyncServer` (port 7900 default) — accepts client connections, broadcasts received messages to all other connected clients
- `ChatSyncClient` — connects to the chat server, relays `ServerChatEvent` messages, receives and broadcasts messages from the server to local players
- Uses exponential backoff for reconnect (5s base, up to 60s, max 10 attempts)
---
### 4.4 `JdbcConfig.java` — Configuration
**Path:** `src/main/java/vip/fubuki/playersync/config/JdbcConfig.java`
All configuration is done via Forge's `ForgeConfigSpec` system (TOML file on server). Key entries:
| Config Key | Default | Description |
|---|---|---|
| `host` | `localhost` | MySQL host |
| `db_port` | `3306` | MySQL port |
| `use_ssl` | `false` | SSL for DB connection |
| `user_name` | `playersync` | DB username |
| `password` | `pleaseChangeThisPassword` | DB password |
| `db_name` | `playersync` | Database name |
| `Server_id` | random int | Unique server identifier |
| `sync_world` | `[]` | World names for advancements (empty = auto-detect) |
| `sync_advancements` | `true` | Enable advancement sync |
| `sync_chat` | `false` | Enable cross-server chat |
| `IsChatServer` | `false` | This server acts as chat relay host |
| `ChatServerIP` | `127.0.0.1` | Chat server address |
| `ChatServerPort` | `7900` | Chat server port |
| `kick_when_already_online` | `true` | Prevent multi-server login |
| `use_legacy_serialization` | `false` | Use old serialization format |
| `item_placeholder_title_override` | `""` | Custom placeholder title |
| `item_placeholder_description_override` | `""` | Custom placeholder description |
| `sync_mekanism_personal_chest` | `false` | Sync Mekanism personal chest inventories |
---
### 4.5 `JDBCsetUp.java` — Database Utility
**Path:** `src/main/java/vip/fubuki/playersync/util/JDBCsetUp.java`
Encapsulates all MySQL operations:
| Method | Purpose |
|---|---|
| `getConnection(selectDatabase)` | Create JDBC connection; `selectDatabase=false` for DDL statements |
| `executeQuery()` | Run SELECT, returns `QueryResult` record |
| `executeUpdate()` | Run INSERT/UPDATE/DELETE/DDL with database |
| `executeUpdateWithoutDatabase()` | Run DDL without default database |
| `update()` | Parameterized update (anti-SQL-injection) |
The `QueryResult` record (`Connection`, `PreparedStatement`, `ResultSet`) implements `AutoCloseable` for try-with-resources support.
**Note:** SQL queries in `VanillaSync` use **string concatenation** for UUID values rather than parameterized queries — this is a potential SQL injection risk if UUIDs could be attacker-controlled (though Minecraft UUIDs are generally safe). The `JDBCsetUp.update()` method does provide parameterized queries but is not used for the main sync queries.
---
### 4.6 `LocalJsonUtil.java` — Lightweight Map Parser
**Path:** `src/main/java/vip/fubuki/playersync/util/LocalJsonUtil.java`
A minimal parser that converts Java `Map.toString()` output back into `Map<String, String>` or `Map<Integer, String>`. Used because the item/effect data is stored as `{0=..., 1=..., 2=...}` format (Java map string representation), not standard JSON.
Uses `=` as key-value separator (not `:`), matching Java's `Map.toString()` format.
---
### 4.7 `PSThreadPoolFactory.java` — Named Thread Factory
**Path:** `src/main/java/vip/fubuki/playersync/util/PSThreadPoolFactory.java`
Simple `ThreadFactory` implementation producing threads named `PlayerSync-thread-N` for easier debugging.
---
### 4.8 `CommandInit.java` — Command Registration
**Path:** `src/main/java/vip/fubuki/playersync/CommandInit.java`
Registers the `/playersync` command (permission level 2 required). Currently has a `reconnect` subcommand that is a stub (returns 0, implementation commented out).
---
### 4.9 Addon Support (`ModsSupport.java` & `CuriosCache.java`)
#### Curios Integration
- **Save:** Serializes all Curios slot items as `{slotType:index=serializedNbt, ...}` map, stored in `curios` table
- **Restore:** Clears all Curios slots, reads DB data, deserializes with placeholder support
- **Death handling:** `CuriosCache` snapshots Curios inventory on death (keepInventory worlds only), used on logout to prevent empty-save bug from the "Title Screen" disconnect case
- Cache entries expire after 1 hour
#### Sophisticated Backpacks Integration
- **Save:** Iterates backpack items via `PlayerInventoryProvider.runOnBackpacks()`, extracts `contentsUuid` via `NBTHelper`, stores NBT in `backpack_data` table using `REPLACE INTO`
- **Restore:** On player join, reads backpack NBT from DB and calls `BackpackStorage.get().setBackpackContents()` to inject it
#### Mekanism Integration
**Jetpack Mode (already synced):** The jetpack mode (`NORMAL`/`HOVER`/`DISABLED`) is stored in the ItemStack NBT at path `mekData.mode`. PlayerSync's existing `serializeNBT()` → Base64 pipeline preserves all ItemStack NBT including `mekData.*`, so jetpack state is automatically synced without any additional code. The `PlayerState.activeJetpacks` set is purely runtime memory (used for sound/particle effects) and is cleared by Mekanism on player logout — it does not need persistence.
**Personal Chest (new sync):** Personal chest inventories are stored in Mekanism's world-level `SavedData` files (`<world>/data/mekanism_personal_storage/<uuid>`), NOT on the ItemStack. The ItemStack only carries a UUID reference (`mekData.personalStorageId`). When a player moves between servers, the ItemStack is restored but the target server's `SavedData` has no inventory for that UUID — the chest appears empty.
`MekanismSupport.java` solves this by:
- **Save:** Iterates the player's inventory + ender chest, finds every item with a `mekData.personalStorageId` tag. Uses reflection to call `PersonalStorageManager.getInventoryIfPresent(stack)`, then serializes all 54 `IInventorySlot` entries via `slot.serializeNBT()` into a single NBT `CompoundTag`, which is Base64-encoded and stored in the `mekanism_personal_chest` table keyed by `storage_id`.
- **Restore:** After inventory is restored (in `doPlayerJoin()`), iterates the player's items again to find personal chests. For each, queries the DB for saved inventory data. Uses reflection to call `PersonalStorageManager.getInventoryFor(stack)` to create/retrieve the `SavedData` entry on the target server, then calls `slot.deserializeNBT()` on each of the 54 slots.
- **Reflection:** `PersonalStorageManager` and `PersonalStorageData` are in Mekanism's implementation JAR (not the API JAR), so access is via `Class.forName()` + `Method.invoke()`. The `IInventorySlot` interface IS in the API JAR and is used directly at compile time.
- **Config:** Controlled by `sync_mekanism_personal_chest` (default `false`).
| Method | Purpose |
|---|---|
| `collectPersonalChestItems(player)` | Scan inventory + ender chest for personal chest items, return map of storageId → ItemStack |
| `savePersonalChestData(player)` | Serialize each chest's 54 slots, write to DB via `REPLACE INTO` |
| `restorePersonalChestData(player)` | Read from DB, call `getInventoryFor()`, deserialize slots into target world's `SavedData` |
| `getInventoryIfPresent(stack)` | Reflection wrapper for `PersonalStorageManager.getInventoryIfPresent()` |
| `getInventoryFor(stack)` | Reflection wrapper for `PersonalStorageManager.getInventoryFor()` |
| `getInventorySlots(inventory)` | Reflection wrapper for `AbstractPersonalStorageItemInventory.getInventorySlots(null)` |
| `getPersonalStorageId(stack)` | Extract `personalStorageId` UUID from ItemStack NBT `mekData` |
## 5. Synchronized Data
| Data | Storage Column | Type | Format |
|---|---|---|---|
| Inventory (36 slots) | `inventory` | MEDIUMBLOB | `{0=B64:..., 1=B64:..., ...}` |
| Armor (4 slots) | `armor` | BLOB | `{0=B64:..., ...}` |
| Off-hand | `left_hand` | BLOB | Base64 NBT string |
| Cursor (carried) | `cursors` | BLOB | Base64 NBT string |
| Ender Chest (27 slots) | `enderchest` | MEDIUMBLOB | `{0=B64:..., ...}` |
| Advancements | `advancements` | MEDIUMBLOB | Raw JSON bytes |
| Active Effects | `effects` | BLOB | `{effectId=B64:nbt, ...}` |
| XP | `xp` | INT | Absolute XP value |
| Health | `health` | INT | HP as integer |
| Food Level | `food_level` | INT | 0-20 |
| Score | `score` | INT | Display score |
| Online Status | `online` | TINYINT | 0 or 1 |
| Last Server | `last_server` | INT | Server ID |
| Curios | `curios_item` (separate table) | BLOB | `{slotType:index=B64:..., ...}` |
| Backpacks | `backpack_nbt` (separate table) | MEDIUMBLOB | Base64 NBT string |
| Mekanism Personal Chest | `inventory_data` (separate table) | MEDIUMBLOB | Base64 NBT of 54 slots (via `IInventorySlot.serializeNBT()`) |
---
## 6. Lifecycle Events Summary
```
Server Starting
├── commonSetup: register JDBC driver, register VanillaSync, (optionally) ChatSync
└── onServerStarting:
├── CREATE DATABASE IF NOT EXISTS
├── CREATE/ALTER player_data table
├── CREATE/ALTER server_info table
├── CREATE curios table (if Curios loaded)
├── CREATE backpack_data table (if SophisticatedBackpacks loaded)
├── CREATE mekanism_personal_chest table (if Mekanism loaded)
├── INSERT/UPDATE server_info (heartbeat)
└── Reset stale online flags
Player Connecting
├── onPlayerConnect: check already-online, kick if needed
├── onDataPackSyncEvent: write advancements JSON to disk
└── onPlayerJoin:
├── Restore: health, food, XP, score
├── Restore: left_hand, cursors, armor, inventory, enderchest, effects
├── Restore: Curios items (ModsSupport)
├── Restore: Backpack data (ModsSupport)
└── Restore: Mekanism personal chest data (MekanismSupport)
Player Playing
├── onPlayerSaveToFile: save() to DB (per save event)
├── onServerTick: auto-save all players every ~1 min
├── onUpdate: heartbeat every ~90s
└── ServerChatEvent → ChatSyncClient → TCP
Player Logging Out
├── onPlayerDeath: cache Curios (if keepInventory)
└── onPlayerLogout:
├── Dead/dying: use CuriosCache, don't save new data
├── Sync incomplete: skip save (safety)
└── Normal: store() + ModsSupport.onPlayerLeave()
Server Shutting Down
├── onServerShutdown: mark server disabled
└── onServerStopping: shutdown ChatSync
```
---
## 7. Key Design Decisions
1. **Thread pool bounded with CallerRunsPolicy** — Fix for #169. Prevents thread explosion under concurrent player load by limiting to 8 threads + 256 queue, with natural backpressure.
2. **Base64 NBT serialization** — Replaced legacy character-replacement format. Backward-compatible read of both formats; config flag to force legacy writes.
3. **Placeholder items for missing mods** — When a modded item's registry entry doesn't exist on the current server (e.g., player moved from Mekanism server to vanilla), a paper voucher preserves the original NBT so the item can be restored when the player returns.
4. **Dead/dying player handling** — Special case for players who die during the login process or disconnect via "Title Screen" while dead. Uses `CuriosCache` to snapshot Curios data on death so it's not lost on logout.
5. **Advancement sync via filesystem** — Rather than direct NBT manipulation, advancements are synced by writing the JSON file to the world's advancements directory and calling `reload()`. This leverages Minecraft's built-in advancement loading.
6. **Schema auto-migration** — The mod checks column existence and data types on startup and alters tables as needed, supporting upgrades without manual SQL.
7. **Server heartbeat system** — Each server writes `last_update` every ~90s. Other servers check this to determine if a player is genuinely online elsewhere (5-minute window) before kicking.
8. **Reflection-based mod addon support** — Third-party mod data whose storage classes are not in the public API JAR (like Mekanism's `PersonalStorageManager`) are accessed via `Class.forName()` + `Method.invoke()`. This avoids hard-compile-dependencies on implementation JARs while still enabling deep data sync at runtime. The compile-time dependency stays on the API JAR only (`IInventorySlot`, `NBTConstants`).

View File

@ -1,4 +1,53 @@
# Sets default memory used for gradle commands. Can be overridden by user or command line properties.
# This is required to provide enough memory for the Minecraft decompilation process.
org.gradle.jvmargs=-Xmx3G
org.gradle.daemon=false
org.gradle.jvmargs=-Xmx1G
org.gradle.daemon=true
org.gradle.parallel=true
org.gradle.caching=true
org.gradle.configuration-cache=true
#read more on this at https://github.com/neoforged/ModDevGradle?tab=readme-ov-file#better-minecraft-parameter-names--javadoc-parchment
# you can also find the latest versions at: https://parchmentmc.org/docs/getting-started
parchment_minecraft_version=1.20.1
parchment_mappings_version=2023.09.03
# Environment Properties
# You can find the latest versions here: https://files.minecraftforge.net/net/minecraftforge/forge/index_1.20.1.html
# The Minecraft version must agree with the Forge version to get a valid artifact
minecraft_version=1.20.1
# The Minecraft version range can use any release version of Minecraft as bounds.
# Snapshots, pre-releases, and release candidates are not guaranteed to sort properly
# as they do not follow standard versioning conventions.
minecraft_version_range=[1.20.1, 1.21)
# The Forge version must agree with the Minecraft version to get a valid artifact
forge_version=47.4.0
# The Forge version range can use any version of Forge as bounds
forge_version_range=[47,)
# The loader version range can only use the major version of FML as bounds
loader_version_range=[47,)
## Mod Properties
# The unique mod identifier for the mod. Must be lowercase in English locale. Must fit the regex [a-z][a-z0-9_]{1,63}
# Must match the String constant located in the main mod class annotated with @Mod.
mod_id=playersync
# The human-readable display name for the mod.
mod_name=PlayerSync
# The license of the mod. Review your options at https://choosealicense.com/. All Rights Reserved is the default.
mod_license=GPL-3.0 license
# The mod version. See https://semver.org/
mod_version=2.1.6
# The group ID for the mod. It is only important when publishing as an artifact to a Maven repository.
# This should match the base package used for the mod sources.
# See https://maven.apache.org/guides/mini/guide-naming-conventions.html
mod_group_id=vip.fubuki.playersync
# The authors of the mod. This is a simple text string that is used for display purposes in the mod list.
mod_authors=mlus
# The description of the mod. This is a simple multiline text string that is used for display purposes in the mod list.
mod_description=make multiserver players' data sync
# JDBC driver version
# see https://dev.mysql.com/doc/relnotes/connector-j/en/ for latest version
jdbc_version=9.3.0
# Mek Version
# see https://modmaven.dev/mekanism/Mekanism/ for latest version
mekanism_version=1.20.1-10.4.9.61

Binary file not shown.

View File

@ -1,5 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.12.1-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

38
gradlew vendored Normal file → Executable file
View File

@ -15,6 +15,8 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
#
##############################################################################
#
@ -55,7 +57,7 @@
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
@ -80,13 +82,12 @@ do
esac
done
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
APP_NAME="Gradle"
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s
' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
@ -133,22 +134,29 @@ location of your Java installation."
fi
else
JAVACMD=java
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
@ -193,11 +201,15 @@ if "$cygwin" || "$msys" ; then
done
fi
# Collect all arguments for the java command;
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
# shell script including quotes and variable substitutions, so put them in
# double quotes to make sure that they get re-expanded; and
# * put everything else in single quotes, so that it's not re-expanded.
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \

185
gradlew.bat vendored
View File

@ -1,91 +1,94 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

View File

@ -1,6 +1,3 @@
pluginManagement {
repositories {
gradlePluginPortal()
maven { url = 'https://maven.minecraftforge.net/' }
}
}
plugins {
id 'org.gradle.toolchains.foojay-resolver-convention' version '0.9.0'
}

View File

@ -0,0 +1,26 @@
package vip.fubuki.playersync;
import com.mojang.brigadier.CommandDispatcher;
import net.minecraft.commands.CommandSourceStack;
import net.minecraft.commands.Commands;
import net.minecraftforge.event.RegisterCommandsEvent;
import net.minecraftforge.eventbus.api.SubscribeEvent;
import net.minecraftforge.fml.common.Mod;
@Mod.EventBusSubscriber()
public class CommandInit {
@SubscribeEvent
public static void registerCommand(RegisterCommandsEvent event){
CommandDispatcher<CommandSourceStack> dispatcher=event.getDispatcher();
dispatcher.register(Commands.literal("playersync")
.requires(cs->cs.hasPermission(2))
.then(Commands.literal("reconnect")
.executes(context -> {
// context.getSource().sendSuccess(()->MutableComponent.create(new TranslatableContents("playersync.command.reconnect")),true);
return 0;
}
))
);
}
}

View File

@ -1,15 +1,17 @@
package vip.fubuki.playersync;
import com.mojang.logging.LogUtils;
import com.mysql.cj.jdbc.Driver;
import net.minecraft.SharedConstants;
import net.minecraftforge.common.MinecraftForge;
import net.minecraftforge.event.server.ServerStartingEvent;
import net.minecraftforge.event.server.ServerStoppingEvent;
import net.minecraftforge.eventbus.api.IEventBus;
import net.minecraftforge.eventbus.api.SubscribeEvent;
import net.minecraftforge.fml.ModList;
import net.minecraftforge.fml.ModLoadingContext;
import net.minecraftforge.fml.common.Mod;
import net.minecraftforge.fml.config.ModConfig;
import net.minecraftforge.fml.event.lifecycle.FMLCommonSetupEvent;
import net.minecraftforge.event.server.ServerStartingEvent;
import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext;
import org.slf4j.Logger;
import vip.fubuki.playersync.config.JdbcConfig;
@ -17,36 +19,242 @@ import vip.fubuki.playersync.sync.ChatSync;
import vip.fubuki.playersync.sync.VanillaSync;
import vip.fubuki.playersync.util.JDBCsetUp;
import java.sql.SQLException;
import java.sql.*;
@Mod(PlayerSync.MODID)
public class PlayerSync
{
public class PlayerSync {
public static final String MODID = "playersync";
public static final Logger LOGGER = LogUtils.getLogger();
public PlayerSync()
{
IEventBus modEventBus = FMLJavaModLoadingContext.get().getModEventBus();
ModLoadingContext.get().registerConfig(ModConfig.Type.COMMON, JdbcConfig.COMMON_CONFIG);
public PlayerSync(FMLJavaModLoadingContext context) {
IEventBus modEventBus = context.getModEventBus();
context.registerConfig(ModConfig.Type.COMMON, JdbcConfig.COMMON_CONFIG);
modEventBus.addListener(this::commonSetup);
MinecraftForge.EVENT_BUS.register(this);
MinecraftForge.EVENT_BUS.register(new VanillaSync());
if(JdbcConfig.SYNC_CHAT.get()){
MinecraftForge.EVENT_BUS.register(new ChatSync());
}
}
private void commonSetup(final FMLCommonSetupEvent event) {}
private void commonSetup(final FMLCommonSetupEvent event) {
// JDBC driver auto-detection is broken in Forge as of v47.4.0
// We need to register the driver manually
try {
DriverManager.registerDriver(new Driver());
} catch (SQLException e) {
LOGGER.error("Unable to register JDBC MySQL driver", e);
}
VanillaSync.register();
event.enqueueWork(() -> {
// read SYNC_CHAT only within the enqueueWork to reliably get the real
// config value and not its default value.
if (JdbcConfig.SYNC_CHAT.get()) {
LOGGER.info("Chat sync enabled.");
ChatSync.register();
}
});
}
@SubscribeEvent
public void onServerStarting(ServerStartingEvent event) throws SQLException, ClassNotFoundException, InstantiationException, IllegalAccessException {
JDBCsetUp.executeUpdate("CREATE DATABASE IF NOT EXISTS "+JdbcConfig.DATABASE_NAME.get(),true);
JDBCsetUp.executeUpdate("CREATE TABLE IF NOT EXISTS player_data (uuid CHAR(36) NOT NULL,inventory BLOB,armor BLOB,advancements BLOB,enderchest BLOB,effects BLOB,xp int,food_level int,score int,health int,online boolean, PRIMARY KEY (uuid))");
JDBCsetUp.executeUpdate("CREATE TABLE IF NOT EXISTS chat (player CHAR(36) NOT NULL,message TEXT,timestamp BIGINT)");
if(ModList.get().isLoaded("curios")) {
JDBCsetUp.executeUpdate("CREATE TABLE IF NOT EXISTS curios (uuid CHAR(36) NOT NULL,curios_item BLOB, PRIMARY KEY (uuid))");
public void onServerStarting(ServerStartingEvent event) throws SQLException {
String dbName = JdbcConfig.DATABASE_NAME.get();
// Step 1: Create the database using a connection that does not select a database.
JDBCsetUp.executeUpdateWithoutDatabase("CREATE DATABASE IF NOT EXISTS `" + dbName + "`");
// Step 2: Explicitly select the database on a connection obtained without default database.
try (Connection conn = JDBCsetUp.getConnection(false);
Statement st = conn.createStatement()) {
st.execute("USE `" + dbName + "`");
} catch (SQLException e) {
LOGGER.error("Error selecting database " + dbName, e);
throw e;
}
// Step 3: Create and alter tables using fully qualified names.
// Create player_data table
JDBCsetUp.executeUpdate(
"CREATE TABLE IF NOT EXISTS `" + dbName + "`.`player_data` (" +
"`uuid` char(36) NOT NULL," +
"`inventory` mediumblob," +
"`armor` blob," +
"`advancements` blob," +
"`enderchest` mediumblob," +
"`effects` blob," +
"`left_hand` blob," +
"`cursors` blob," +
"`xp` int DEFAULT NULL," +
"`food_level` int DEFAULT NULL," +
"`score` int DEFAULT NULL," +
"`health` int DEFAULT NULL," +
"`online` tinyint(1) DEFAULT NULL," +
"`last_server` int DEFAULT NULL," +
"PRIMARY KEY (`uuid`)" +
");"
);
// Check and alter player_data table if columns are missing
JDBCsetUp.QueryResult queryResult = JDBCsetUp.executeQuery(
"SELECT COUNT(*) AS column_count " +
"FROM INFORMATION_SCHEMA.COLUMNS " +
"WHERE TABLE_SCHEMA = '" + dbName + "' " +
"AND TABLE_NAME = 'player_data';"
);
ResultSet resultSet = queryResult.resultSet();
int columnCount = 0;
if (resultSet.next()) {
columnCount = resultSet.getInt("column_count");
}
if (columnCount < 14) {
JDBCsetUp.executeUpdate(
"ALTER TABLE `" + dbName + "`.`player_data` " +
"ADD COLUMN left_hand blob, " +
"ADD COLUMN cursors blob;"
);
}
// Create server_info table
JDBCsetUp.executeUpdate(
"CREATE TABLE IF NOT EXISTS `" + dbName + "`.`server_info` (" +
"`id` INT NOT NULL," +
"`enable` boolean NOT NULL," +
"`last_update` BIGINT NOT NULL," +
"PRIMARY KEY (`id`)" +
");"
);
// do not modify the create table statement to make sure this code is compatible with older database versions
addColumnIfNotExists("server_info", "data_version", "INT NOT NULL DEFAULT 0");
long current = System.currentTimeMillis();
int data_version = SharedConstants.getCurrentVersion().getDataVersion().getVersion();
JDBCsetUp.executeUpdate("""
INSERT INTO `%s`.`server_info`
(
id,
enable,
data_version,
last_update
)
VALUES (
%d,
true,
%d,
%d
)
ON DUPLICATE KEY UPDATE
id = %d,
enable = true,
data_version = %d,
last_update = %d;
""",
dbName,
JdbcConfig.SERVER_ID.get(),
data_version,
current,
JdbcConfig.SERVER_ID.get(),
data_version,
current);
// Create curios table if the Curios mod is loaded
if (ModList.get().isLoaded("curios")) {
JDBCsetUp.executeUpdate(
"CREATE TABLE IF NOT EXISTS `" + dbName + "`.`curios` (" +
"uuid CHAR(36) NOT NULL, curios_item BLOB, PRIMARY KEY (uuid)" +
")"
);
}
// Create backpack_data table
if (ModList.get().isLoaded("sophisticatedbackpacks")) {
JDBCsetUp.executeUpdateWithoutDatabase(
"CREATE TABLE IF NOT EXISTS `" + dbName + "`.`backpack_data` (" +
"uuid CHAR(36) NOT NULL, backpack_nbt MEDIUMBLOB, PRIMARY KEY (uuid)" +
");"
);
// Check if backpack_data table has the 'uuid' column
addColumnIfNotExists("backpack_data", "uuid", "CHAR(36) NOT NULL", true);
}
// Create mekanism_personal_chest table if Mekanism is loaded
if (ModList.get().isLoaded("mekanism")) {
JDBCsetUp.executeUpdate(
"CREATE TABLE IF NOT EXISTS `" + dbName + "`.`mekanism_personal_chest` (" +
"player_uuid CHAR(36) NOT NULL," +
"storage_id CHAR(36) NOT NULL," +
"inventory_data MEDIUMBLOB," +
"PRIMARY KEY (storage_id)," +
"INDEX idx_player_uuid (player_uuid)" +
");"
);
}
// Check and alter the 'advancements' column in player_data if necessary
JDBCsetUp.QueryResult advColCheck = JDBCsetUp.executeQuery(
"SELECT DATA_TYPE FROM INFORMATION_SCHEMA.COLUMNS " +
"WHERE TABLE_SCHEMA = '" + dbName + "' " +
"AND TABLE_NAME = 'player_data' " +
"AND COLUMN_NAME = 'advancements';"
);
ResultSet rsAdvCol = advColCheck.resultSet();
if (rsAdvCol.next()) {
String dataType = rsAdvCol.getString("DATA_TYPE");
if (!"mediumblob".equalsIgnoreCase(dataType)) {
LOGGER.info("Altering player_data table to modify 'advancements' column from {} to MEDIUMBLOB.", dataType);
JDBCsetUp.executeUpdateWithoutDatabase("ALTER TABLE `" + dbName + "`.`player_data` MODIFY COLUMN advancements MEDIUMBLOB");
}
}
rsAdvCol.close();
// ----- END NEW BLOCK -----
try {
JDBCsetUp.executeUpdate("UPDATE player_data SET online=0 WHERE last_server=" + JdbcConfig.SERVER_ID.get() +" AND online=1 LIMIT 1000");
} catch (Exception e) {
LOGGER.error("An exception occurred while trying change wrong player-status\n" + e.getMessage());
}
LOGGER.info("PlayerSync is ready!");
}
@SubscribeEvent
public void onServerStopping(ServerStoppingEvent event){
ChatSync.shutdown();
}
private static void addColumnIfNotExists(String tableName, String columnName, String dataTypeDefaultNullness,
boolean makePrimaryKey) throws SQLException {
// Making use of the AutoCloseable QueryResult here
try (JDBCsetUp.QueryResult backpackColCheck = JDBCsetUp.executeQuery(
"SELECT COUNT(*) AS colCount FROM INFORMATION_SCHEMA.COLUMNS " +
"WHERE TABLE_SCHEMA = DATABASE()" +
"AND TABLE_NAME = '" + tableName + "' " +
"AND COLUMN_NAME = '" + columnName + "';")) {
ResultSet rsBackpackCol = backpackColCheck.resultSet();
if (!rsBackpackCol.next()) {
LOGGER.warn("Warning: Unable to check existence of colum {} in table {}.", columnName, tableName);
return;
}
if (rsBackpackCol.getInt("colCount") > 0) {
LOGGER.debug("Column {} already exists. Skipping creation.", columnName);
return;
}
}
LOGGER.info("ALTER {} table to add missing {} column.", tableName, columnName);
// Add the missing column and set it as primary key.
JDBCsetUp.executeUpdate(
"ALTER TABLE %s ADD COLUMN %s %s",
tableName, columnName, dataTypeDefaultNullness);
if (makePrimaryKey) {
LOGGER.info("Altering {} table to add primary key on {}.", tableName, columnName);
JDBCsetUp.executeUpdate(
"ALTER TABLE %s ADD PRIMARY KEY (%s)",
tableName, columnName);
}
}
private static void addColumnIfNotExists(String tableName, String columnName,
String dataTypeDefaultNullness) throws SQLException {
addColumnIfNotExists(tableName, columnName, dataTypeDefaultNullness, false);
}
}

View File

@ -4,35 +4,69 @@ package vip.fubuki.playersync.config;
import net.minecraftforge.common.ForgeConfigSpec;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Random;
public class JdbcConfig {
public static ForgeConfigSpec COMMON_CONFIG;
public static ForgeConfigSpec.ConfigValue<String> HOST;
public static ForgeConfigSpec.ConfigValue<String> DATABASE_NAME;
public static ForgeConfigSpec.IntValue PORT;
public static ForgeConfigSpec.ConfigValue<String> USERNAME;
public static ForgeConfigSpec.ConfigValue<String> PASSWORD;
public static ForgeConfigSpec.ConfigValue<List<String>> SYNC_WORLD;
public static ForgeConfigSpec.BooleanValue USE_SSL;
public static ForgeConfigSpec.BooleanValue SYNC_CHAT;
public static final ForgeConfigSpec COMMON_CONFIG;
public static final ForgeConfigSpec.ConfigValue<String> HOST;
public static final ForgeConfigSpec.IntValue PORT;
public static final ForgeConfigSpec.ConfigValue<String> USERNAME;
public static final ForgeConfigSpec.ConfigValue<String> PASSWORD;
public static final ForgeConfigSpec.ConfigValue<String> DATABASE_NAME;
public static final ForgeConfigSpec.ConfigValue<List<String>> SYNC_WORLD;
public static final ForgeConfigSpec.BooleanValue SYNC_ADVANCEMENTS;
public static final ForgeConfigSpec.BooleanValue USE_SSL;
public static final ForgeConfigSpec.BooleanValue SYNC_CHAT;
public static final ForgeConfigSpec.BooleanValue IS_CHAT_SERVER;
public static final ForgeConfigSpec.ConfigValue<String> ITEM_PLACEHOLDER_TITLE_OVERRIDE;
public static final ForgeConfigSpec.ConfigValue<String> ITEM_PLACEHOLDER_DESCRIPTION_OVERRIDE;
public static final ForgeConfigSpec.BooleanValue KICK_WHEN_ALREADY_ONLINE;
public static final ForgeConfigSpec.ConfigValue<String> CHAT_SERVER_IP;
public static final ForgeConfigSpec.IntValue CHAT_SERVER_PORT;
public static final ForgeConfigSpec.BooleanValue USE_LEGACY_SERIALIZATION;
public static final ForgeConfigSpec.BooleanValue SYNC_MEKANISM_PERSONAL_CHEST;
public static final ForgeConfigSpec.ConfigValue<Integer> SERVER_ID;
static {
ForgeConfigSpec.Builder COMMON_BUILDER = new ForgeConfigSpec.Builder();
COMMON_BUILDER.comment("General settings").push("general");
HOST=COMMON_BUILDER.comment("The host of the database").define("host", "localhost");
DATABASE_NAME= COMMON_BUILDER.comment("Database name").define("database_name", "playersync");
PORT = COMMON_BUILDER.comment("database port").defineInRange("db_port", 3306, 0, 65535);
USE_SSL = COMMON_BUILDER.comment("whether use SSL").define("use_ssl", false);
USERNAME = COMMON_BUILDER.comment("username").define("user_name", "root");
PASSWORD = COMMON_BUILDER.comment("password").define("password", "password");
SYNC_WORLD = COMMON_BUILDER.comment("The worlds that will be synchronized.If running in server it is supposed to have only one").define("sync_world", new ArrayList<String>());
SYNC_CHAT= COMMON_BUILDER.comment("Whether synchronize chat").define("sync_chat", true);
USERNAME = COMMON_BUILDER.comment("username").define("user_name", "playersync");
PASSWORD = COMMON_BUILDER.comment("password").define("password", "pleaseChangeThisPassword");
DATABASE_NAME = COMMON_BUILDER.comment("database name").define("db_name","playersync");
SERVER_ID = COMMON_BUILDER.comment("the server id should be unique").define("Server_id", new Random().nextInt(1,Integer.MAX_VALUE-1));
SYNC_WORLD = COMMON_BUILDER.comment("The worlds that will be synchronized. If running on a server, leave array empty.").define("sync_world", new ArrayList<>());
SYNC_ADVANCEMENTS = COMMON_BUILDER.comment("Whether to sync advancements between servers")
.define("sync_advancements", true);
SYNC_CHAT = COMMON_BUILDER.comment("Whether synchronize chat").define("sync_chat", false);
IS_CHAT_SERVER = COMMON_BUILDER.comment("Whether recieve messages from other servers as host").define("IsChatServer",false);
KICK_WHEN_ALREADY_ONLINE = COMMON_BUILDER.comment("Whether to kick player when already online on another server")
.define("kick_when_already_online", true);
CHAT_SERVER_IP = COMMON_BUILDER.define("ChatServerIP","127.0.0.1");
CHAT_SERVER_PORT = COMMON_BUILDER.defineInRange("ChatServerPort",7900,0,65535);
USE_LEGACY_SERIALIZATION = COMMON_BUILDER.comment(
"Use the old (pre-Base64) serialization format for writing data to the database.",
"Set to true ONLY if you have older mod versions reading the same database.",
"This only affects writing data, the mod can read both Base64 and pre-Base64 serialization.",
"New installations should leave this as 'false'."
).define("use_legacy_serialization", false);
ITEM_PLACEHOLDER_TITLE_OVERRIDE = COMMON_BUILDER
.comment("Override the title of placeholder items which are unavailable on the current server.")
.define("item_placeholder_title_override", "");
ITEM_PLACEHOLDER_DESCRIPTION_OVERRIDE = COMMON_BUILDER
.comment("Override the description of placeholder items which are unavailable on the current server.")
.define("item_placeholder_description_override", "");
SYNC_MEKANISM_PERSONAL_CHEST = COMMON_BUILDER
.comment("Whether to sync Mekanism personal chest inventories across servers.")
.define("sync_mekanism_personal_chest", false);
COMMON_BUILDER.pop();
COMMON_CONFIG = COMMON_BUILDER.build();
}
}

View File

@ -1,41 +1,55 @@
package vip.fubuki.playersync.sync;
import net.minecraft.network.chat.Component;
import net.minecraft.server.players.PlayerList;
import net.minecraftforge.eventbus.api.SubscribeEvent;
import vip.fubuki.playersync.util.JDBCsetUp;
import com.mojang.logging.LogUtils;
import net.minecraftforge.common.MinecraftForge;
import org.slf4j.Logger;
import vip.fubuki.playersync.config.JdbcConfig;
import vip.fubuki.playersync.sync.chat.ChatSyncClient;
import vip.fubuki.playersync.sync.chat.ChatSyncServer;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Objects;
import java.io.IOException;
public class ChatSync {
static int tick = 0;
static long current = System.currentTimeMillis();
@SubscribeEvent
public static void onPlayerChat(net.minecraftforge.event.ServerChatEvent event) throws SQLException, ClassNotFoundException, InstantiationException, IllegalAccessException {
ReadMessage(Objects.requireNonNull(event.getPlayer().getServer()).getPlayerList());
JDBCsetUp.executeUpdate("INSERT INTO chat (player, message, timestamp) VALUES ('" + event.getUsername() + "', '" + event.getMessage() + "', '" + current + "')");
public static final Logger LOGGER = LogUtils.getLogger();
private static ChatSyncServer chatSyncServer;
private static ChatSyncClient chatSyncClient;
public static void register(){
if(JdbcConfig.IS_CHAT_SERVER.get()) {
LOGGER.info("Trying to setup chat server at port " + JdbcConfig.CHAT_SERVER_PORT.get());
new Thread(()->{
chatSyncServer = new ChatSyncServer();
try {
chatSyncServer.run();
} catch (IOException e) {
LOGGER.error("Unable to start chat server", e);
}
}, "ChatSync-Server").start();
}
new Thread(()->{
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
LOGGER.info("Trying to connect to chat server "
+ JdbcConfig.CHAT_SERVER_IP.get()
+ ":"
+ JdbcConfig.CHAT_SERVER_PORT.get());
chatSyncClient = new ChatSyncClient();
chatSyncClient.run();
}, "ChatSync-Client").start();
MinecraftForge.EVENT_BUS.register(ChatSyncClient.class);
}
@SubscribeEvent
public static void Tick(net.minecraftforge.event.TickEvent.ServerTickEvent event) throws SQLException, ClassNotFoundException, InstantiationException, IllegalAccessException {
tick++;
if(tick == 20) {
ReadMessage(event.getServer().getPlayerList());
public static void shutdown() {
if (chatSyncServer != null) {
chatSyncServer.shutdown();
}
}
public static void ReadMessage(PlayerList playerList) throws SQLException, ClassNotFoundException, InstantiationException, IllegalAccessException {
ResultSet resultSet= JDBCsetUp.executeQuery("SELECT * FROM chat WHERE timestamp > " + current);
current = System.currentTimeMillis();
tick = 0;
while(resultSet.next()) {
String player = resultSet.getString("player");
String message = resultSet.getString("message");
Component textComponents = Component.literal(player+": "+message);
playerList.broadcastSystemMessage(textComponents, true);
if (chatSyncClient != null) {
chatSyncClient.shutdown();
}
resultSet.close();
}
}

View File

@ -1,76 +0,0 @@
package vip.fubuki.playersync.sync;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
import net.minecraft.nbt.NbtUtils;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.ItemStack;
import net.minecraftforge.common.util.LazyOptional;
import net.minecraftforge.fml.ModList;
import net.minecraftforge.items.IItemHandlerModifiable;
import vip.fubuki.playersync.util.JDBCsetUp;
import vip.fubuki.playersync.util.LocalJsonUtil;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.Map;
@SuppressWarnings({"InstantiationOfUtilityClass", "AccessStaticViaInstance"})
public class ModsSupport {
public void onPlayerJoin(Player player) throws SQLException, ClassNotFoundException, InstantiationException, IllegalAccessException {
if (ModList.get().isLoaded("curios")) {
//TODO curios support
top.theillusivec4.curios.api.CuriosApi CuriosApi = new top.theillusivec4.curios.api.CuriosApi();
LazyOptional<IItemHandlerModifiable> itemHandler = CuriosApi.getCuriosHelper().getEquippedCurios(player);
ResultSet resultSet = JDBCsetUp.executeQuery("SELECT curios_item FROM curios WHERE uuid = '"+player.getUUID()+"'");
if(resultSet.next()) {
Map<Integer, String> curios = LocalJsonUtil.StringToEntryMap(resultSet.getString("curios_item"));
itemHandler.ifPresent(handler -> {
for (int i = 0; i < handler.getSlots(); i++) {
try {
if(curios.get(i)==null) continue;
handler.setStackInSlot(i, ItemStack.of(NbtUtils.snbtToStructure(curios.get(i).replace("|", ","))));
} catch (CommandSyntaxException e) {
throw new RuntimeException(e);
}
}
});
resultSet.close();
}else{
StoreCurios(player,true);
}
}
if(ModList.get().isLoaded("sophisticatedbackpacks")) {
//TODO sophisticatedbackpacks support
}
}
public void onPlayerLeave(Player player) throws SQLException, ClassNotFoundException, InstantiationException, IllegalAccessException {
if (ModList.get().isLoaded("curios")) {
StoreCurios(player, false);
}
if(ModList.get().isLoaded("sophisticatedbackpacks")) {
//TODO sophisticatedbackpacks support
}
}
public void StoreCurios(Player player,boolean init) throws SQLException, ClassNotFoundException, InstantiationException, IllegalAccessException {
top.theillusivec4.curios.api.CuriosApi CuriosApi = new top.theillusivec4.curios.api.CuriosApi();
LazyOptional<IItemHandlerModifiable> itemHandler = CuriosApi.getCuriosHelper().getEquippedCurios(player);
Map<Integer, String> curios = new HashMap<>();
itemHandler.ifPresent(handler -> {
for (int i = 0; i < handler.getSlots(); i++) {
if (!handler.getStackInSlot(i).isEmpty()) {
String sNBT= handler.getStackInSlot(i).serializeNBT().toString().replace(",", "|");
curios.put(i, sNBT);
}
}
});
if(init) {
JDBCsetUp.executeUpdate("INSERT INTO curios (uuid,curios_item) VALUES ('"+player.getUUID()+"','"+ curios+"')");
} else {
JDBCsetUp.executeUpdate("UPDATE curios SET curios_item = '"+ curios+"' WHERE uuid = '"+player.getUUID()+"'");
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,122 @@
package vip.fubuki.playersync.sync.addons;
import net.minecraft.server.MinecraftServer;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.GameRules;
import net.minecraftforge.common.util.LazyOptional;
import net.minecraftforge.fml.ModList;
import top.theillusivec4.curios.api.CuriosApi;
import top.theillusivec4.curios.api.type.capability.ICuriosItemHandler;
import top.theillusivec4.curios.api.type.inventory.IDynamicStackHandler;
import vip.fubuki.playersync.PlayerSync;
import vip.fubuki.playersync.sync.VanillaSync;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
public class CuriosCache {
private static final long CACHE_EXPIRY_MS = 3600000;
public static final ConcurrentHashMap<UUID, CuriosCacheEntry> curiosCache = new ConcurrentHashMap<>();
public static class CuriosCacheEntry {
final long timeStamp;
final String serializedData;
CuriosCacheEntry(String data) {
this.timeStamp = System.currentTimeMillis();
this.serializedData = data;
}
boolean isExpired() {
return System.currentTimeMillis() - timeStamp > CACHE_EXPIRY_MS;
}
}
//If player logged out by "Title Screen" button,you will not be able to get the handlerOpt,and it will make the curios inventory sync failed.
//Create a method to store temporary curios data when player is dead.
//Then check player status in the logged out event,and take a normal sync if player is alive.
//If player is dead or dying,the cache will be used to prevent the empty data from the failure of getting handlerOpt.
public static void tryStoreCuriosToCache(net.minecraft.world.entity.player.Player player) {
if (!ModList.get().isLoaded("curios") || !CuriosCache.isKeepInventoryActive(player)) {
return;
}
try {
LazyOptional<ICuriosItemHandler> handlerOpt = CuriosApi.getCuriosInventory(player);
if (!handlerOpt.isPresent() || handlerOpt.resolve().isEmpty()) {
PlayerSync.LOGGER.error("Obtain the curios api failed,cannot create the cache.");
return;
}
ICuriosItemHandler handler = handlerOpt.resolve().get();
String serializedData = serializeCuriosInventory(handler);
if (serializedData.startsWith("{}")) {
PlayerSync.LOGGER.debug("No curios data found,skipping the step of creating cache");
return;
}
UUID playerUuid = player.getUUID();
curiosCache.put(playerUuid, new CuriosCacheEntry(serializedData));
} catch (Exception e) {
PlayerSync.LOGGER.error("An error occurred while creating curios cache:" + e.getMessage());
}
}
private static String serializeCuriosInventory(ICuriosItemHandler handler) {
Map<String, String> flatMap = new HashMap<>();
try {
handler.getCurios().forEach((slotType, stacksHandler) -> {
IDynamicStackHandler dynStacks = stacksHandler.getStacks();
for (int i = 0; i < dynStacks.getSlots(); i++) {
ItemStack stack = dynStacks.getStackInSlot(i);
if (!stack.isEmpty()) {
String serialized = VanillaSync.serialize(VanillaSync.serializeNBT(stack).toString());
flatMap.put(slotType + ":" + i, serialized);
}
}
});
} catch (Exception e) {
PlayerSync.LOGGER.error("Failed to serialize curios data:" + e.getMessage());
}
return flatMap.isEmpty() ? "{}" : flatMap.toString();
}
public static boolean isKeepInventoryActive(Player player) {
MinecraftServer server = player.getServer();
if (server == null) {
PlayerSync.LOGGER.error("Trying to get the gamerule(KeepInventory),but server is null");
return false;
}
return server.getGameRules().getBoolean(GameRules.RULE_KEEPINVENTORY);
}
public static void RemoveExpiredCuriosCache() {
long startMs = System.currentTimeMillis();
int cacheSize = curiosCache.size();
if (cacheSize == 0) {
PlayerSync.LOGGER.debug("No curios caches,skipping cleaning");
return;
}
int removed = 0;
Iterator<Map.Entry<UUID, CuriosCacheEntry>> iterator = curiosCache.entrySet().iterator();
while (iterator.hasNext()) {
if (iterator.next().getValue().isExpired()) {
iterator.remove();;
removed ++;
}
}
if (removed > 0) {
PlayerSync.LOGGER.info("Cleaned {} curios cache(s),{} left,took {} Ms",
removed, curiosCache.size(), System.currentTimeMillis() - startMs);
}
}
}

View File

@ -0,0 +1,221 @@
package vip.fubuki.playersync.sync.addons;
import mekanism.api.inventory.IInventorySlot;
import mekanism.common.lib.inventory.personalstorage.AbstractPersonalStorageItemInventory;
import mekanism.common.lib.inventory.personalstorage.PersonalStorageManager;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.ListTag;
import net.minecraft.nbt.Tag;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.ItemStack;
import net.minecraftforge.fml.ModList;
import vip.fubuki.playersync.PlayerSync;
import vip.fubuki.playersync.config.JdbcConfig;
import vip.fubuki.playersync.util.JDBCsetUp;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.*;
public class MekanismSupport {
private static final String MEK_DATA = "mekData";
private static final String PERSONAL_STORAGE_ID = "personalStorageId";
/**
* Call PersonalStorageManager.getInventoryIfPresent(stack) directly.
*/
static Optional<AbstractPersonalStorageItemInventory> getInventoryIfPresent(ItemStack stack) {
try {
return PersonalStorageManager.getInventoryIfPresent(stack);
} catch (Exception e) {
PlayerSync.LOGGER.error("Error calling PersonalStorageManager.getInventoryIfPresent", e);
return Optional.empty();
}
}
static Optional<AbstractPersonalStorageItemInventory> getInventoryFor(ItemStack stack) {
try {
return PersonalStorageManager.getInventoryFor(stack);
} catch (Exception e) {
PlayerSync.LOGGER.error("Error calling PersonalStorageManager.getInventoryFor", e);
return Optional.empty();
}
}
/**
* Get inventory slots directly from PersonalStorageInventory.
*/
static List<IInventorySlot> getInventorySlots(AbstractPersonalStorageItemInventory inventory) {
try {
return inventory.getInventorySlots(null);
} catch (Exception e) {
PlayerSync.LOGGER.error("Error calling getInventorySlots", e);
return Collections.emptyList();
}
}
/**
* Collect all personal chest ItemStacks from a player's inventory + ender chest.
* Returns map: personalStorageId UUID string ItemStack
*/
public static Map<String, ItemStack> collectPersonalChestItems(Player player) {
Map<String, ItemStack> result = new LinkedHashMap<>();
List<ItemStack> allItems = new ArrayList<>();
// Main inventory
for (int i = 0; i < player.getInventory().items.size(); i++) {
allItems.add(player.getInventory().items.get(i));
}
// Armor
for (int i = 0; i < player.getInventory().armor.size(); i++) {
allItems.add(player.getInventory().armor.get(i));
}
// Off-hand
allItems.add(player.getInventory().offhand.get(0));
// Ender chest
for (int i = 0; i < player.getEnderChestInventory().getContainerSize(); i++) {
allItems.add(player.getEnderChestInventory().getItem(i));
}
for (ItemStack stack : allItems) {
String storageId = getPersonalStorageId(stack);
if (storageId != null) {
result.put(storageId, stack);
}
}
return result;
}
/**
* Extract the personalStorageId UUID from an ItemStack's mekData NBT.
*/
public static String getPersonalStorageId(ItemStack stack) {
if (stack.isEmpty())
return null;
CompoundTag tag = stack.getTag();
if (tag == null || !tag.contains(MEK_DATA, Tag.TAG_COMPOUND))
return null;
CompoundTag mekData = tag.getCompound(MEK_DATA);
if (mekData.contains(PERSONAL_STORAGE_ID)) {
return mekData.getString(PERSONAL_STORAGE_ID);
}
return null;
}
/**
* Save all personal chest inventory data to the database.
*/
public static void savePersonalChestData(Player player) throws SQLException {
if (!ModList.get().isLoaded("mekanism") || !JdbcConfig.SYNC_MEKANISM_PERSONAL_CHEST.get())
return;
Map<String, ItemStack> chestItems = collectPersonalChestItems(player);
if (chestItems.isEmpty())
return;
for (Map.Entry<String, ItemStack> entry : chestItems.entrySet()) {
String storageId = entry.getKey();
ItemStack stack = entry.getValue();
Optional<AbstractPersonalStorageItemInventory> invOpt = getInventoryIfPresent(stack);
if (invOpt.isEmpty())
continue;
AbstractPersonalStorageItemInventory inventory = invOpt.get();
List<IInventorySlot> slots = getInventorySlots(inventory);
// Serialize all slots into a CompoundTag
CompoundTag rootTag = new CompoundTag();
ListTag slotList = new ListTag();
for (int i = 0; i < slots.size(); i++) {
IInventorySlot slot = slots.get(i);
CompoundTag slotTag = slot.serializeNBT();
slotTag.putInt("SlotIndex", i);
slotList.add(slotTag);
}
rootTag.put("Slots", slotList);
rootTag.putInt("SlotCount", slots.size());
String serialized = vip.fubuki.playersync.sync.VanillaSync.serialize(rootTag.toString());
JDBCsetUp.executeUpdate(
"REPLACE INTO mekanism_personal_chest (player_uuid, storage_id, inventory_data) VALUES ('%s', '%s', '%s')",
player.getUUID().toString(),
storageId,
serialized);
}
PlayerSync.LOGGER.debug("Saved {} personal chest(s) for player {}",
chestItems.size(), player.getUUID());
}
/**
* Restore personal chest inventory data from the database.
* Should be called AFTER inventory/enderchest items are restored.
*/
public static void restorePersonalChestData(Player player) throws SQLException {
if (!ModList.get().isLoaded("mekanism") || !JdbcConfig.SYNC_MEKANISM_PERSONAL_CHEST.get())
return;
Map<String, ItemStack> chestItems = collectPersonalChestItems(player);
if (chestItems.isEmpty())
return;
int restored = 0;
for (Map.Entry<String, ItemStack> entry : chestItems.entrySet()) {
String storageId = entry.getKey();
ItemStack stack = entry.getValue();
// Query saved data from DB
JDBCsetUp.QueryResult qr = JDBCsetUp.executeQuery(
"SELECT inventory_data FROM mekanism_personal_chest WHERE storage_id = '%s'",
storageId);
ResultSet rs = qr.resultSet();
if (!rs.next()) {
rs.close();
qr.connection().close();
continue;
}
String serialized = rs.getString("inventory_data");
rs.close();
qr.connection().close();
if (serialized == null || serialized.isEmpty())
continue;
// Deserialize
String nbtString = vip.fubuki.playersync.sync.VanillaSync.deserializeString(serialized);
CompoundTag rootTag;
try {
rootTag = net.minecraft.nbt.TagParser.parseTag(nbtString);
} catch (Exception e) {
PlayerSync.LOGGER.error("Failed to parse personal chest NBT for storage {}", storageId, e);
continue;
}
// Ensure inventory exists in target world's SavedData
Optional<AbstractPersonalStorageItemInventory> invOpt = getInventoryFor(stack);
if (invOpt.isEmpty())
continue;
AbstractPersonalStorageItemInventory inventory = invOpt.get();
List<IInventorySlot> slots = getInventorySlots(inventory);
ListTag slotList = rootTag.getList("Slots", Tag.TAG_COMPOUND);
for (int i = 0; i < slotList.size(); i++) {
CompoundTag slotTag = slotList.getCompound(i);
int slotIndex = slotTag.getInt("SlotIndex");
slotTag.remove("SlotIndex");
if (slotIndex >= 0 && slotIndex < slots.size()) {
slots.get(slotIndex).deserializeNBT(slotTag);
}
}
restored++;
}
if (restored > 0) {
PlayerSync.LOGGER.info("Restored {} personal chest(s) for player {}",
restored, player.getUUID());
}
}
}

View File

@ -0,0 +1,221 @@
package vip.fubuki.playersync.sync.addons;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.NbtUtils;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.ItemStack;
import net.minecraftforge.common.util.LazyOptional;
import net.minecraftforge.fml.ModList;
import top.theillusivec4.curios.api.CuriosApi;
import top.theillusivec4.curios.api.type.capability.ICuriosItemHandler;
import top.theillusivec4.curios.api.type.inventory.ICurioStacksHandler;
import top.theillusivec4.curios.api.type.inventory.IDynamicStackHandler;
import vip.fubuki.playersync.PlayerSync;
import vip.fubuki.playersync.sync.VanillaSync;
import vip.fubuki.playersync.util.JDBCsetUp;
import vip.fubuki.playersync.util.LocalJsonUtil;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
public class ModsSupport {
public void doBackPackRestore(Player player) {
if(ModList.get().isLoaded("sophisticatedbackpacks")){
// --- Begin Backpack Data Restore ---
PlayerSync.LOGGER.info("Restoring backpack data for player " + player.getUUID());
net.p3pp3rf1y.sophisticatedbackpacks.util.PlayerInventoryProvider.get().runOnBackpacks(player, (ItemStack backpackItem, String handler, String identifier, int slot) -> {
backpackItem.getCapability(net.p3pp3rf1y.sophisticatedbackpacks.api.CapabilityBackpackWrapper.getCapabilityInstance())
.ifPresent(wrapper -> {
// Retrieve the contents UUID from the backpack's NBT using NBTHelper
Optional<UUID> uuidOpt = net.p3pp3rf1y.sophisticatedcore.util.NBTHelper.getUniqueId(wrapper.getBackpack(), "contentsUuid");
if (uuidOpt.isPresent()) {
UUID contentsUuid = uuidOpt.get();
try {
JDBCsetUp.QueryResult qrBackpack = JDBCsetUp.executeQuery("SELECT backpack_nbt FROM backpack_data WHERE uuid='" + contentsUuid + "'");
ResultSet rsBackpack = qrBackpack.resultSet();
if (rsBackpack.next()) {
String serialized = rsBackpack.getString("backpack_nbt");
String nbtString = VanillaSync.deserializeString(serialized);
CompoundTag backpackNbt = NbtUtils.snbtToStructure(nbtString);
// Update BackpackStorage with the retrieved NBT
net.p3pp3rf1y.sophisticatedbackpacks.backpack.BackpackStorage.get().setBackpackContents(contentsUuid, backpackNbt);
PlayerSync.LOGGER.info("Restored backpack data for UUID " + contentsUuid);
}
rsBackpack.close();
qrBackpack.connection().close();
} catch (SQLException e) {
PlayerSync.LOGGER.error("Error restoring backpack data for UUID " + contentsUuid, e);
} catch (CommandSyntaxException e) {
throw new RuntimeException(e);
}
} else {
PlayerSync.LOGGER.warn("Backpack item in slot " + slot + " has no contentsUuid during restore");
}
});
return false;
});
// --- End Backpack Data Restore ---
}
}
/**
* Restores the Curios inventory for a player.
* The saved data is stored as a flat map with composite keys ("slotType:index").
*/
public void doCuriosRestore(Player player) throws SQLException {
if (ModList.get().isLoaded("curios")) {
// Obtain the handler from the API.
LazyOptional<ICuriosItemHandler> handlerOpt = CuriosApi.getCuriosInventory(player);
JDBCsetUp.QueryResult qr = JDBCsetUp.executeQuery("SELECT curios_item FROM curios WHERE uuid = '" + player.getUUID() + "'");
ResultSet rs = qr.resultSet();
if (rs.next()) {
String curiosData = rs.getString("curios_item");
// Parse the stored data (assumes a simple Map.toString() format: "{key=value, key2=value2, ...}")
Map<String, String> storedMap = LocalJsonUtil.StringToMap(curiosData);
// Clear current Curios slots to avoid conflicts.
handlerOpt.ifPresent(handler -> handler.getCurios().forEach((slotType, stacksHandler) -> {
// Use the dynamic stack handler to clear slots.
IDynamicStackHandler dynStacks = stacksHandler.getStacks();
for (int i = 0; i < dynStacks.getSlots(); i++) {
dynStacks.setStackInSlot(i, ItemStack.EMPTY);
}
}));
if (curiosData.length() <= 2) {
rs.close();
qr.connection().close();
return;
}
// Restore each saved item.
handlerOpt.ifPresent(handler -> {
for (Map.Entry<String, String> entry : storedMap.entrySet()) {
String compositeKey = entry.getKey(); // Expected format: "slotType:index"
String[] parts = compositeKey.split(":");
if (parts.length != 2) {
continue;
}
String slotType = parts[0];
int slotIndex;
try {
slotIndex = Integer.parseInt(parts[1]);
} catch (NumberFormatException ex) {
continue;
}
String serialized = entry.getValue();
try {
ItemStack stack = VanillaSync.deserializeAndCreatePlaceholderIfNeeded(serialized);
if (handler.getCurios().containsKey(slotType)) {
ICurioStacksHandler stacksHandler = handler.getCurios().get(slotType);
IDynamicStackHandler dynStacks = stacksHandler.getStacks();
if (slotIndex < dynStacks.getSlots()) {
dynStacks.setStackInSlot(slotIndex, stack);
}
}
} catch (CommandSyntaxException e) {
throw new RuntimeException("Error deserializing Curio data for key " + compositeKey, e);
}
}
});
rs.close();
qr.connection().close();
} else {
// No stored data; perform an initial save.
StoreCurios(player, true);
}
}
}
/**
* Saves the current Curios inventory for a player.
* It builds a flat map keyed by "slotType:index" using the dynamic stack handler.
*/
public void onPlayerLeave(net.minecraft.world.entity.player.Player player) throws SQLException {
if (ModList.get().isLoaded("curios")) {
if (player.isDeadOrDying()) {
if (!CuriosCache.curiosCache.isEmpty()) {
UUID playerUuid = player.getUUID();
if (CuriosCache.curiosCache.get(playerUuid) != null) {
CuriosCache.CuriosCacheEntry cacheEntry = CuriosCache.curiosCache.get(playerUuid);
String serializedData = cacheEntry.serializedData;
JDBCsetUp.executeUpdate("UPDATE curios SET curios_item = '" + serializedData + "' WHERE uuid = '" + player.getUUID() + "'");
CuriosCache.curiosCache.remove(playerUuid);
PlayerSync.LOGGER.info("Saving curios data for a dead-or-dying player {} Successfully", player.getStringUUID());
} else {
PlayerSync.LOGGER.error("Failed to find the cache of the logged out dead-or-dying player");
PlayerSync.LOGGER.error("The dead-or-dying player uuid is" + player.getStringUUID());
PlayerSync.LOGGER.error("Using default data...");
StoreCurios(player, false);
}
} else {
PlayerSync.LOGGER.warn("No curios cache found while executing a dead-or-dying player logout event.you can ignore this warning if keep-inventory is false");
PlayerSync.LOGGER.warn("The dead-or-dying player uuid is" + player.getStringUUID());
PlayerSync.LOGGER.warn("Using default data...");
StoreCurios(player, false);
}
} else {
StoreCurios(player, false);
}
}
}
public void StoreCurios(net.minecraft.world.entity.player.Player player, boolean init) throws SQLException {
LazyOptional<ICuriosItemHandler> handlerOpt = CuriosApi.getCuriosInventory(player);
Map<String, String> flatMap = new HashMap<>();
handlerOpt.ifPresent(handler -> {
// Iterate over each slot type.
handler.getCurios().forEach((slotType, stacksHandler) -> {
IDynamicStackHandler dynStacks = stacksHandler.getStacks();
for (int i = 0; i < dynStacks.getSlots(); i++) {
ItemStack stack = dynStacks.getStackInSlot(i);
if (!stack.isEmpty()) {
String serialized = VanillaSync.serialize(VanillaSync.serializeNBT(stack).toString());
flatMap.put(slotType + ":" + i, serialized);
}
}
});
});
String serializedData = flatMap.toString();
if (init) {
JDBCsetUp.executeUpdate("INSERT INTO curios (uuid,curios_item) VALUES ('" + player.getUUID() + "', '" + serializedData + "')");
} else {
JDBCsetUp.executeUpdate("UPDATE curios SET curios_item = '" + serializedData + "' WHERE uuid = '" + player.getUUID() + "'");
}
}
public static void storeSophisticatedBackpacks(Player player) {
PlayerSync.LOGGER.info("Storing backpack data for player " + player.getUUID());
net.p3pp3rf1y.sophisticatedbackpacks.util.PlayerInventoryProvider.get().runOnBackpacks(player, (ItemStack backpackItem, String handler, String identifier, int slot) -> {
backpackItem.getCapability(net.p3pp3rf1y.sophisticatedbackpacks.api.CapabilityBackpackWrapper.getCapabilityInstance())
.ifPresent(wrapper -> {
// Retrieve the contents UUID from the backpack's NBT using NBTHelper
Optional<UUID> uuidOpt = net.p3pp3rf1y.sophisticatedcore.util.NBTHelper.getUniqueId(wrapper.getBackpack(), "contentsUuid");
if (uuidOpt.isPresent()) {
UUID contentsUuid = uuidOpt.get();
// Get internal backpack data from BackpackStorage (creates it if missing)
CompoundTag backpackNbt = net.p3pp3rf1y.sophisticatedbackpacks.backpack.BackpackStorage.get().getOrCreateBackpackContents(contentsUuid);
String serialized = VanillaSync.serialize(backpackNbt.toString());
try {
// Use REPLACE INTO so existing records are updated
JDBCsetUp.executeUpdate("REPLACE INTO backpack_data (uuid, backpack_nbt) VALUES ('" + contentsUuid + "', '" + serialized + "')");
PlayerSync.LOGGER.info("Saved backpack data for UUID " + contentsUuid);
} catch (SQLException e) {
PlayerSync.LOGGER.error("Error saving backpack data for UUID " + contentsUuid, e);
}
} else {
PlayerSync.LOGGER.warn("Backpack item in slot " + slot + " has no contentsUuid");
}
});
return false; // Continue processing all backpack items.
});
}
}

View File

@ -0,0 +1,133 @@
package vip.fubuki.playersync.sync.chat;
import net.minecraft.network.chat.Component;
import net.minecraft.server.players.PlayerList;
import net.minecraftforge.event.entity.player.PlayerEvent;
import net.minecraftforge.eventbus.api.SubscribeEvent;
import vip.fubuki.playersync.PlayerSync;
import vip.fubuki.playersync.config.JdbcConfig;
import java.io.*;
import java.net.ConnectException;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketTimeoutException;
import java.util.Objects;
public class ChatSyncClient {
static PlayerList playerList;
static Socket clientSocket;
static PrintWriter out;
private static volatile boolean running = true;
private static final int RECONNECT_DELAY = 5000;
private static final int MAX_RECONNECT_ATTEMPTS = 10;
public void run() {
int reconnectAttempts = 0;
while (running && reconnectAttempts < MAX_RECONNECT_ATTEMPTS) {
try {
PlayerSync.LOGGER.info("Connecting to chat server {}:{}",
JdbcConfig.CHAT_SERVER_IP.get(),
JdbcConfig.CHAT_SERVER_PORT.get());
clientSocket = new Socket();
clientSocket.setReuseAddress(true);
clientSocket.setKeepAlive(true);
clientSocket.setTcpNoDelay(true);
clientSocket.connect(
new InetSocketAddress(
JdbcConfig.CHAT_SERVER_IP.get(),
JdbcConfig.CHAT_SERVER_PORT.get()
),
15000
);
clientSocket.setSoTimeout(0);
out = new PrintWriter(new BufferedWriter(
new OutputStreamWriter(clientSocket.getOutputStream())), true);
PlayerSync.LOGGER.info("Successfully connected to chat server");
reconnectAttempts = 0;
BufferedReader in = new BufferedReader(
new InputStreamReader(clientSocket.getInputStream()));
String serverMessage;
while (running && (serverMessage = in.readLine()) != null) {
Component textComponents = Component.nullToEmpty(serverMessage);
if(playerList != null){
playerList.getServer().execute(() ->
playerList.broadcastSystemMessage(textComponents, false));
}else {
PlayerSync.LOGGER.info("Received message from chat server: " + serverMessage);
}
}
} catch (SocketTimeoutException e) {
PlayerSync.LOGGER.warn("Chat server read timeout, reconnecting...");
} catch (ConnectException e) {
PlayerSync.LOGGER.warn("Cannot connect to chat server: {}", e.getMessage());
} catch (IOException e) {
PlayerSync.LOGGER.error("Chat client connection error: {}", e.getMessage());
} finally {
closeConnection();
}
if (running && reconnectAttempts < MAX_RECONNECT_ATTEMPTS) {
reconnectAttempts++;
PlayerSync.LOGGER.warn("Attempting to reconnect to chat server ({}/{})",
reconnectAttempts, MAX_RECONNECT_ATTEMPTS);
try {
long delay = Math.min(RECONNECT_DELAY * (long)Math.pow(2, reconnectAttempts-1), 60000);
Thread.sleep(delay);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
}
}
private void closeConnection() {
try {
if (out != null) {
out.close();
out = null;
}
if (clientSocket != null && !clientSocket.isClosed()) {
clientSocket.close();
clientSocket = null;
}
} catch (IOException e) {
PlayerSync.LOGGER.error("Error closing connection: {}", e.getMessage());
}
}
public void shutdown() {
running = false;
closeConnection();
}
@SubscribeEvent
public static void onPlayerChat(net.minecraftforge.event.ServerChatEvent event) {
String message= "<"+event.getUsername()+"> "+event.getMessage().getString();
if (out != null) {
out.println(message);
}
}
@SubscribeEvent
public static void onPlayerJoin(PlayerEvent.PlayerLoggedInEvent event){
playerList = Objects.requireNonNull(event.getEntity().getServer()).getPlayerList();
}
@SubscribeEvent
public static void onPlayerLeave(PlayerEvent.PlayerLoggedOutEvent event){
playerList = Objects.requireNonNull(event.getEntity().getServer()).getPlayerList();
}
}

View File

@ -0,0 +1,130 @@
package vip.fubuki.playersync.sync.chat;
import vip.fubuki.playersync.PlayerSync;
import vip.fubuki.playersync.config.JdbcConfig;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketTimeoutException;
import java.util.Iterator;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
public class ChatSyncServer {
static ServerSocket serverSocket;
static final Set<Socket> SocketList = ConcurrentHashMap.newKeySet();
static final ExecutorService executorService = Executors.newCachedThreadPool();
private volatile boolean running = true;
public void run() throws IOException {
try {
serverSocket = new ServerSocket(JdbcConfig.CHAT_SERVER_PORT.get());
serverSocket.setReuseAddress(true);
PlayerSync.LOGGER.info("Chat server started successfully on port {}", JdbcConfig.CHAT_SERVER_PORT.get());
while (running && !Thread.currentThread().isInterrupted()) {
try {
Socket newSocket = serverSocket.accept();
newSocket.setSoTimeout(0);
SocketList.add(newSocket);
executorService.submit(() -> handleClient(newSocket));
PlayerSync.LOGGER.info("New client connected, total clients: {}", SocketList.size());
} catch (IOException e) {
if (running) {
PlayerSync.LOGGER.error("Error accepting client connection: {}", e.getMessage());
}
}
}
} finally {
shutdown();
}
}
private void handleClient(Socket socket) {
String clientInfo = socket.getInetAddress() + ":" + socket.getPort();
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(socket.getInputStream()))) {
String message;
while (running && (message = reader.readLine()) != null) {
broadcastMessage(socket, message);
}
} catch (SocketTimeoutException e) {
PlayerSync.LOGGER.warn("Client {} timeout", clientInfo);
} catch (IOException e) {
PlayerSync.LOGGER.error("Error handling client {}: {}", clientInfo, e.getMessage());
} finally {
SocketList.remove(socket);
try {
if (!socket.isClosed()) {
socket.close();
}
} catch (IOException e) {
PlayerSync.LOGGER.error("Error closing client socket: {}", e.getMessage());
}
PlayerSync.LOGGER.info("Client disconnected, remaining clients: {}", SocketList.size());
}
}
private void broadcastMessage(Socket sender, String message) {
Iterator<Socket> iterator = SocketList.iterator();
while (iterator.hasNext()) {
Socket socket = iterator.next();
if (!socket.equals(sender) && !socket.isClosed()) {
try {
PrintWriter writer = new PrintWriter(socket.getOutputStream(), true);
writer.println(message);
} catch (IOException e) {
PlayerSync.LOGGER.error("Error broadcasting to client, removing: {}", e.getMessage());
iterator.remove();
try {
socket.close();
} catch (IOException ex) {
// Ignore
}
}
}
}
}
public void shutdown() {
running = false;
try {
if (serverSocket != null && !serverSocket.isClosed()) {
serverSocket.close();
}
} catch (IOException e) {
PlayerSync.LOGGER.error("Error closing server socket: {}", e.getMessage());
}
for (Socket socket : SocketList) {
try {
if (!socket.isClosed()) {
socket.close();
}
} catch (IOException e) {
// Ignore
}
}
SocketList.clear();
executorService.shutdown();
try {
if (!executorService.awaitTermination(5, TimeUnit.SECONDS)) {
executorService.shutdownNow();
}
} catch (InterruptedException e) {
executorService.shutdownNow();
Thread.currentThread().interrupt();
}
}
}

View File

@ -1,46 +1,125 @@
package vip.fubuki.playersync.util;
import com.mojang.logging.LogUtils;
import org.slf4j.Logger;
import vip.fubuki.playersync.config.JdbcConfig;
import java.sql.*;
import java.util.Properties;
public class JDBCsetUp {
private static final Logger LOGGER = LogUtils.getLogger();
@SuppressWarnings("deprecation")
public static Connection getConnection(boolean init) throws InstantiationException, IllegalAccessException, ClassNotFoundException, SQLException {
Class<?> clazz = Class.forName("com.mysql.cj.jdbc.Driver");
Driver driver = (Driver) clazz.newInstance();
Properties properties = new Properties();
properties.setProperty("user",JdbcConfig.USERNAME.get());
properties.setProperty("password",JdbcConfig.PASSWORD.get());
String url;
if(init) {
url="jdbc:mysql://"+JdbcConfig.HOST.get()+":"+JdbcConfig.PORT.get()+"?useUnicode=true&characterEncoding=utf-8&useSSL="+JdbcConfig.USE_SSL.get()+"&serverTimezone=UTC&allowPublicKeyRetrieval=true";
}else{
url="jdbc:mysql://"+JdbcConfig.HOST.get()+":"+JdbcConfig.PORT.get()+"/"+JdbcConfig.DATABASE_NAME.get()+"?useUnicode=true&characterEncoding=utf-8&useSSL="+JdbcConfig.USE_SSL.get()+"&serverTimezone=UTC&allowPublicKeyRetrieval=true";
/**
* Returns a connection to the MySQL server.
* @param selectDatabase if true, the returned URL includes the configured database name.
* @return a Connection object with the database explicitly selected.
* @throws SQLException if a database access error occurs.
*/
public static Connection getConnection(boolean selectDatabase) throws SQLException {
String dbName = JdbcConfig.DATABASE_NAME.get();
// Build the base URL
String url = "jdbc:mysql://" + JdbcConfig.HOST.get() + ":" + JdbcConfig.PORT.get();
if (selectDatabase && dbName != null && !dbName.isEmpty()) {
url += "/" + dbName;
}
return driver.connect(url,properties);
url += "?useUnicode=true&characterEncoding=utf-8&useSSL=" + JdbcConfig.USE_SSL.get()
+ "&serverTimezone=UTC&allowPublicKeyRetrieval=true";
Connection conn = DriverManager.getConnection(url, JdbcConfig.USERNAME.get(), JdbcConfig.PASSWORD.get());
// Ensure that the connection uses the desired database by explicitly issuing "USE dbName"
if (selectDatabase && dbName != null && !dbName.isEmpty()) {
try (Statement st = conn.createStatement()) {
st.execute("USE `" + dbName + "`");
}
}
return conn;
}
public static ResultSet executeQuery(String sql) throws SQLException, InstantiationException, IllegalAccessException, ClassNotFoundException {
Statement statement= getConnection(false).createStatement();
return statement.executeQuery(sql);
// Default connection always includes the database.
public static Connection getConnection() throws SQLException {
return getConnection(true);
}
public static void executeUpdate(String sql) throws SQLException, InstantiationException, IllegalAccessException, ClassNotFoundException {
Statement statement= getConnection(false).createStatement();
statement.executeUpdate(sql);
statement.close();
/**
* Executes a query using a connection that includes the database.
*/
public static QueryResult executeQuery(String sqlFormatString, Object... args) throws SQLException {
String sql = String.format(sqlFormatString, args);
LOGGER.trace(sql);
Connection connection = getConnection(); // With database selected (and "USE" already run)
PreparedStatement queryStatement = connection.prepareStatement(sql);
ResultSet resultSet = queryStatement.executeQuery();
return new QueryResult(connection, queryStatement, resultSet);
}
public static void executeUpdate(String sql,boolean init) throws SQLException, InstantiationException, IllegalAccessException, ClassNotFoundException {
Statement statement= getConnection(init).createStatement();
if(!init) statement.executeUpdate("USE "+JdbcConfig.DATABASE_NAME.get());
statement.executeUpdate(sql);
statement.close();
/**
* Executes an update using a connection with or without the database within the JDBC URL
*/
private static void executeUpdate(boolean selectDatabase, String sqlFormatString, Object... args) throws SQLException {
String sql = String.format(sqlFormatString, args);
LOGGER.trace(sql);
try (Connection connection = getConnection(selectDatabase)) {
try (PreparedStatement updateStatement = connection.prepareStatement(sql)) {
updateStatement.executeUpdate();
}
}
}
/**
* Executes an update using a connection that includes the database in the JDBC URL
*/
public static void executeUpdate(String sqlFormatString, Object... args) throws SQLException {
executeUpdate(true, sqlFormatString, args);
}
/**
* Executes an update using a connection that does NOT include a default database.
* This method is used for commands like "CREATE DATABASE IF NOT EXISTS ..."
*/
public static void executeUpdateWithoutDatabase(String sqlFormatString, Object... args) throws SQLException {
executeUpdate(false, sqlFormatString, args);
}
/**
* A helper method for updates with parameters.
*/
public static void update(String sql, String... argument) throws SQLException {
LOGGER.trace(sql);
try (Connection connection = getConnection()) { // With database selected
PreparedStatement updateStatement = connection.prepareStatement(sql);
for (int i = 0; i < argument.length; i++) {
updateStatement.setString(i + 1, argument[i]);
}
updateStatement.executeUpdate();
}
}
public record QueryResult(Connection connection,PreparedStatement preparedStatement, ResultSet resultSet) implements AutoCloseable {
@Override
public void close() {
if (resultSet != null) {
try {
resultSet.close();
} catch (SQLException e) {
LOGGER.error("Error closing ResultSet", e);
}
}
if (preparedStatement != null) {
try {
preparedStatement.close();
} catch (SQLException e) {
LOGGER.error("Error closing PreparedStatement", e);
}
}
if (connection != null) {
try {
connection.close();
} catch (SQLException e) {
LOGGER.error("Error closing Connection", e);
}
}
}
}
}

View File

@ -2,31 +2,44 @@ package vip.fubuki.playersync.util;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;
public class LocalJsonUtil {
public static Map<String,String> StringToMap(String param) {
Map<String,String> map = new HashMap<>();
String s1 = param.substring(1,param.length()-1);
String s2 = s1.trim();
String[] split = s2.split(",");
for (int i = split.length - 1; i >= 0; i--) {
String trim = split[i].trim();
String[] split1 = trim.split("=");
map.put(split1[0],split1[1]);
private static <K> Map<K, String> stringToGenericMap(String param, Function<String, K> keyParser) {
Map<K, String> map = new HashMap<>();
// check if string is at least minimal json
if (param == null || param.length() < 2 || param.equals("{}")) {
return map;
}
// extract string within outermost json brackets {}
String s1 = param.substring(param.indexOf('{')+1, param.lastIndexOf('}')).trim();
if (s1.isEmpty()) {
return map;
}
// split all json elements
for (String split : s1.split(",")) {
String trim = split.trim();
// only check for the first "=" as the values also contain additional "="
int equalIndex = trim.indexOf('=');
if (equalIndex < 0)
continue;
String key = trim.substring(0, equalIndex);
String value = trim.substring(equalIndex + 1);
map.put(keyParser.apply(key), value);
}
return map;
}
public static Map<Integer,String> StringToEntryMap(String param) {
Map<Integer,String> map = new HashMap<>();
String s1 = param.substring(1,param.length()-1);
String s2 = s1.trim();
String[] split = s2.split(",");
for (int i = split.length - 1; i >= 0; i--) {
String trim = split[i].trim();
String[] split1 = trim.split("=");
map.put(Integer.parseInt(split1[0]),split1[1]);
}
return map;
public static Map<String, String> StringToMap(String param) {
return stringToGenericMap(param, Function.identity());
}
public static Map<Integer, String> StringToEntryMap(String param) {
return stringToGenericMap(param, Integer::parseInt);
}
}

View File

@ -0,0 +1,21 @@
package vip.fubuki.playersync.util;
import javax.annotation.Nonnull;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.atomic.AtomicInteger;
public class PSThreadPoolFactory implements ThreadFactory {
private final AtomicInteger threadIdx = new AtomicInteger(0);
private final String threadNamePrefix;
public PSThreadPoolFactory(String Prefix) {
threadNamePrefix = Prefix;
}
@Override
public Thread newThread(@Nonnull Runnable runnable) {
Thread thread = new Thread(runnable);
thread.setName(threadNamePrefix + "-thread-" + threadIdx.getAndIncrement());
return thread;
}
}

View File

@ -1,3 +0,0 @@
{
"playersync.already_online": "You cannot join more than one synchronization server at the same time."
}

View File

@ -1,3 +0,0 @@
{
"playersync.already_online": "你不能同时加入一个以上的同步服务器。"
}

View File

@ -0,0 +1,8 @@
{
"playersync.item_placeholder_description": "Item is unknown on this server. This can either\nbe a modded item, an added, or a removed vanilla\nitem.\nThis voucher will automatically be replaced with\nthe corresponding item when joining a server\nwhere the item is known.",
"playersync.placeholder_titel_override": "Item Voucher",
"playersync.item_placeholder_title": "Item Voucher",
"playersync.already_online": "You can't join more than one synchronization server at the same time.",
"playersync.sqlexception": "SqlException detected!Connection lost,please contact with your admin.",
"playersync.wrong_entity_status": "An error occurred while creating playerEntity in the world,please login again."
}

View File

@ -0,0 +1,8 @@
{
"playersync.item_placeholder_description": "在这个服务器上,该物品是未知的\n它可能曾是一个其他已被移除的mod的物品,或者被移除的原版物品\n这一凭证将在加入拥有该物品的服务器时自动转换为对应物品",
"playersync.placeholder_titel_override": "未知物品凭证",
"playersync.item_placeholder_title": "未知物品凭证",
"playersync.already_online": "你不能同时加入多个在线的数据互通的服务器",
"playersync.sqlexception": "检测到Sql异常!连接已中断,请联系管理员",
"playersync.wrong_entity_status": "在世界中尝试创建玩家实体时发生了错误,请尝试重新进入"
}

BIN
src/main/resources/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

View File

@ -1,8 +0,0 @@
{
"pack": {
"description": "examplemod resources",
"pack_format": 9,
"forge:resource_pack_format": 9,
"forge:data_pack_format": 10
}
}

View File

@ -5,62 +5,83 @@
# Find more information on toml format here: https://github.com/toml-lang/toml
# The name of the mod loader type to load - for regular FML @Mod mods it should be javafml
modLoader="javafml" #mandatory
# A version range to match for said mod loader - for regular FML @Mod it will be the forge version
loaderVersion="[43,)" #mandatory This is typically bumped every Minecraft version by Forge. See our download page for lists of versions.
loaderVersion="${loader_version_range}" #mandatory This is typically bumped every Minecraft version by Forge. See our download page for lists of versions.
# The license for you mod. This is mandatory metadata and allows for easier comprehension of your redistributive properties.
# Review your options at https://choosealicense.com/. All rights reserved is the default copyright stance, and is thus the default here.
license="GPL-3.0 license"
license="${mod_license}"
# A URL to refer people to when problems occur with this mod
#issueTrackerURL="https://change.me.to.your.issue.tracker.example.invalid/" #optional
issueTrackerURL="https://github.com/mlus-asuka/PlayerSync/issues"
# A list of mods - how many allowed here is determined by the individual mod loader
[[mods]] #mandatory
# The modid of the mod
modId="playersync" #mandatory
# The version number of the mod - there's a few well known ${} variables useable here or just hardcode it
# ${file.jarVersion} will substitute the value of the Implementation-Version as read from the mod's JAR file metadata
# see the associated build.gradle script for how to populate this completely automatically during a build
version="1.0" #mandatory
# A display name for the mod
displayName="PlayerSync" #mandatory
# A URL to query for updates for this mod. See the JSON update specification https://mcforge.readthedocs.io/en/latest/gettingstarted/autoupdate/
modId="${mod_id}" #mandatory
# The version number of the mod
version="${mod_version}" #mandatory
# A display name for the mod
displayName="${mod_name}" #mandatory
# A URL to query for updates for this mod. See the JSON update specification https://docs.minecraftforge.net/en/latest/misc/updatechecker/
#updateJSONURL="https://change.me.example.invalid/updates.json" #optional
# A URL for the "homepage" for this mod, displayed in the mod UI
displayURL="https://github.com/mlus-asuka/PlayerSync"
# A file name (in the root of the mod JAR) containing a logo for display
logoFile="logo.png" #optional
# A text field displayed in the mod UI
credits="Based on Mysql" #optional
# A text field displayed in the mod UI
authors="mlus" #optional
authors="${mod_authors}" #optional
# Display Test controls the display for your mod in the server connection screen
# MATCH_VERSION means that your mod will cause a red X if the versions on client and server differ. This is the default behaviour and should be what you choose if you have server and client elements to your mod.
# IGNORE_SERVER_VERSION means that your mod will not cause a red X if it's present on the server but not on the client. This is what you should use if you're a server only mod.
# IGNORE_ALL_VERSION means that your mod will not cause a red X if it's present on the client or the server. This is a special case and should only be used if your mod has no server component.
# NONE means that no display test is set on your mod. You need to do this yourself, see IExtensionPoint.DisplayTest for more information. You can define any scheme you wish with this value.
# IMPORTANT NOTE: this is NOT an instruction as to which environments (CLIENT or DEDICATED SERVER) your mod loads on. Your mod should load (and maybe do nothing!) whereever it finds itself.
#displayTest="MATCH_VERSION" # MATCH_VERSION is the default if nothing is specified (#optional)
displayTest="IGNORE_SERVER_VERSION"
# The description text for the mod (multi line!) (#mandatory)
description='''
make multiserver players' data sync
'''
description='''${mod_description}'''
# A dependency - use the . to indicate dependency for a specific modid. Dependencies are optional.
[[dependencies.playersync]] #optional
[[dependencies.${mod_id}]] #optional
# the modid of the dependency
modId="forge" #mandatory
# Does this dependency have to exist - if not, ordering below must be specified
mandatory=true #mandatory
# Optional field describing why the dependency is required or why it is incompatible
# reason="..."
# The version range of the dependency
versionRange="[43,)" #mandatory
# An ordering relationship for the dependency - BEFORE or AFTER required if the relationship is not mandatory
versionRange="${forge_version_range}" #mandatory
# An ordering relationship for the dependency.
# BEFORE - This mod is loaded BEFORE the dependency
# AFTER - This mod is loaded AFTER the dependency
ordering="NONE"
# Side this dependency is applied on - BOTH, CLIENT or SERVER
# Side this dependency is applied on - BOTH, CLIENT, or SERVER
side="BOTH"
# Here's another dependency
[[dependencies.playersync]]
[[dependencies.${mod_id}]]
modId="minecraft"
mandatory=true
# This version range declares a minimum of the current minecraft version up to but not including the next major version
versionRange="[1.19.2,1.20)"
# This version range declares a minimum of the current minecraft version up to but not including the next major version
versionRange="${minecraft_version_range}"
ordering="NONE"
side="BOTH"
side="BOTH"
# Features are specific properties of the game environment, that you may want to declare you require. This example declares
# that your mod requires GL version 3.2 or higher. Other features will be added. They are side aware so declaring this won't
# stop your mod loading on the server for example.
#[features.${mod_id}]
#openGLVersion="[3.2,)"

View File

@ -0,0 +1,8 @@
{
"pack": {
"description": {
"text": "${mod_id} resources"
},
"pack_format": 15
}
}