Skip to content

Commit 87411ab

Browse files
committed
Add conditional tax system across economy modules
1 parent 3074cb2 commit 87411ab

22 files changed

Lines changed: 517 additions & 72 deletions

File tree

CHANGELOG.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,29 @@ All notable changes to MatrixShop will be documented in this file.
44

55
The format is based on Keep a Changelog, and this project follows Semantic Versioning for release tags.
66

7+
## [1.5.0] - 2026-04-03
8+
9+
### Added
10+
11+
- Added a conditional tax system for `PlayerShop`, `GlobalMarket`, `Auction`, `Transaction`, and `ChestShop`.
12+
- Added tax rule support for `Enabled`, `Mode`, `Value`, `Priority`, and Kether `Condition`.
13+
- Added legacy-compatible tax parsing for `GlobalMarket` `Listing.Tax-Percent`.
14+
15+
### Changed
16+
17+
- `PlayerShop`, `GlobalMarket`, and `Auction` now resolve seller-side tax from a shared conditional tax engine instead of fixed inline math.
18+
- `Transaction` now applies money tax per offer direction and records net received amounts.
19+
- `ChestShop` now supports tax on both player-buy and player-sell flows.
20+
- Updated bundled settings and docs to show the new tax rule structure.
21+
22+
### Validated
23+
24+
- Verified `./gradlew build`.
25+
- Verified docs site `npm run build`.
26+
- Verified smoke boot on `paper-1.21.8`.
27+
- Verified smoke boot on `paper-1.21.11`.
28+
- Verified `paper-1.21.11` can load conditional tax configs with Kether `Condition` rules for `PlayerShop`, `GlobalMarket`, `Auction`, `Transaction`, and `ChestShop`.
29+
730
## [1.4.0] - 2026-04-03
831

932
### Added

README.md

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,8 @@ MatrixShop is a modular commerce plugin for survival and economy servers. It pro
4343

4444
| 版本 | 兼容性 |
4545
| --- | --- |
46-
| `1.4.0` | `Paper 1.21.8` smoke boot 通过 |
47-
| `1.4.0` | `Paper 1.21.11` smoke boot 通过 |
46+
| `1.5.0` | `Paper 1.21.8` smoke boot 通过 |
47+
| `1.5.0` | `Paper 1.21.11` smoke boot 通过 |
4848

4949
## SystemShop 重点
5050

@@ -103,6 +103,41 @@ price:
103103
- 支持 `whitelist`、`blacklist` 控制折扣重叠
104104
- 刷新池价格对象会和商品本体折扣规则合并
105105

106+
## 条件税系统
107+
108+
以下模块现在支持条件税配置:
109+
110+
- `PlayerShop`
111+
- `GlobalMarket`
112+
- `Auction`
113+
- `Transaction`
114+
- `ChestShop`
115+
116+
税规则当前支持:
117+
118+
- `Enabled`
119+
- `Mode`
120+
- `Value`
121+
- `Priority`
122+
- `Condition`
123+
124+
示例:
125+
126+
```yaml
127+
Tax:
128+
Enabled: true
129+
Mode: percent
130+
Value: 3.0
131+
Rules:
132+
vip:
133+
Enabled: true
134+
Priority: 100
135+
Mode: percent
136+
Value: 1.0
137+
Condition:
138+
- "perm 'group.vip'"
139+
```
140+
106141
## 构建与运行信息
107142

108143
- Build target: `Bukkit API 1.12.2`
@@ -120,7 +155,7 @@ price:
120155
运行产物:
121156

122157
```text
123-
build/libs/MatrixShop-1.4.0-all.jar
158+
build/libs/MatrixShop-1.5.0-all.jar
124159
```
125160

126161
## 文档入口
@@ -135,4 +170,4 @@ build/libs/MatrixShop-1.4.0-all.jar
135170
## Search Keywords
136171

137172
- English: Minecraft shop plugin, GUI shop plugin, auction plugin, player market plugin, trade plugin, Vault economy plugin, Paper plugin, Folia plugin
138-
- 中文: 商店插件, GUI 商店插件, 拍卖插件, 玩家市场插件, 交易插件, 经济插件, 中文服插件, Paper 插件, Folia 插件
173+
- 中文: 商店插件, GUI 商店插件, 拍卖插件, 玩家市场插件, 交易插件, 经济插件, 中文服插件, Paper 插件, Folia 插件

gradle.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
group=com.y54895.matrixshop
2-
version=1.4.0
2+
version=1.5.0
33
matrixlibApiVersion=1.0.1
44
kotlin.incremental=true
55
kotlin.incremental.java=true
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
package com.y54895.matrixshop.core.economy
2+
3+
import org.bukkit.configuration.ConfigurationSection
4+
import org.bukkit.entity.Player
5+
import taboolib.common.platform.function.warning
6+
import taboolib.module.kether.KetherShell
7+
import taboolib.module.kether.ScriptOptions
8+
import java.util.Locale
9+
import java.util.concurrent.TimeUnit
10+
11+
data class ConditionalTaxRule(
12+
val id: String,
13+
val enabled: Boolean,
14+
val priority: Int,
15+
val mode: String?,
16+
val value: Double?,
17+
val condition: List<String>
18+
)
19+
20+
data class ConditionalTaxConfig(
21+
val enabled: Boolean,
22+
val mode: String,
23+
val value: Double,
24+
val rules: List<ConditionalTaxRule> = emptyList()
25+
)
26+
27+
data class ConditionalTaxResult(
28+
val amount: Double,
29+
val mode: String,
30+
val value: Double,
31+
val ruleId: String?
32+
)
33+
34+
object ConditionalTaxEngine {
35+
36+
fun parse(
37+
root: ConfigurationSection,
38+
path: String,
39+
defaultEnabled: Boolean,
40+
defaultMode: String,
41+
defaultValue: Double,
42+
legacyPercentPath: String? = null
43+
): ConditionalTaxConfig {
44+
val section = root.getConfigurationSection(path)
45+
if (section == null) {
46+
if (legacyPercentPath != null && root.contains(legacyPercentPath)) {
47+
val value = root.getDouble(legacyPercentPath, defaultValue).coerceAtLeast(0.0)
48+
return ConditionalTaxConfig(
49+
enabled = value > 0.0,
50+
mode = "percent",
51+
value = value
52+
)
53+
}
54+
return ConditionalTaxConfig(
55+
enabled = defaultEnabled,
56+
mode = defaultMode,
57+
value = defaultValue.coerceAtLeast(0.0)
58+
)
59+
}
60+
val rulesSection = section.getConfigurationSection("Rules")
61+
val rules = rulesSection?.getKeys(false)?.map { id ->
62+
val ruleSection = rulesSection.getConfigurationSection(id)!!
63+
ConditionalTaxRule(
64+
id = id,
65+
enabled = ruleSection.getBoolean("Enabled", true),
66+
priority = ruleSection.getInt("Priority", 0),
67+
mode = ruleSection.getString("Mode")
68+
?.trim()
69+
?.takeIf(String::isNotBlank),
70+
value = ruleSection.get("Value")?.let { raw ->
71+
when (raw) {
72+
is Number -> raw.toDouble()
73+
is String -> raw.trim().toDoubleOrNull()
74+
else -> null
75+
}
76+
}?.coerceAtLeast(0.0),
77+
condition = readStringList(ruleSection, "Condition", "Conditions", "condition", "conditions")
78+
)
79+
}.orEmpty()
80+
return ConditionalTaxConfig(
81+
enabled = section.getBoolean("Enabled", defaultEnabled),
82+
mode = section.getString("Mode", defaultMode).orEmpty().ifBlank { defaultMode },
83+
value = section.getDouble("Value", defaultValue).coerceAtLeast(0.0),
84+
rules = rules
85+
)
86+
}
87+
88+
fun compute(
89+
config: ConditionalTaxConfig,
90+
amount: Double,
91+
actor: Player?,
92+
moduleId: String,
93+
context: Map<String, Any?> = emptyMap()
94+
): ConditionalTaxResult {
95+
val normalizedAmount = amount.coerceAtLeast(0.0)
96+
if (normalizedAmount <= 0.0 || !config.enabled) {
97+
return ConditionalTaxResult(0.0, config.mode, config.value, null)
98+
}
99+
val matchedRule = config.rules
100+
.sortedWith(compareByDescending<ConditionalTaxRule> { it.priority }.thenBy { it.id })
101+
.firstOrNull { rule ->
102+
rule.enabled && matches(actor, moduleId, rule, context)
103+
}
104+
val mode = matchedRule?.mode ?: config.mode
105+
val value = matchedRule?.value ?: config.value
106+
val taxAmount = when (mode.lowercase(Locale.ROOT)) {
107+
"percent", "rate" -> normalizedAmount * value / 100.0
108+
"fixed", "flat" -> value
109+
else -> value
110+
}.coerceAtLeast(0.0).coerceAtMost(normalizedAmount)
111+
return ConditionalTaxResult(
112+
amount = taxAmount,
113+
mode = mode,
114+
value = value,
115+
ruleId = matchedRule?.id
116+
)
117+
}
118+
119+
private fun matches(
120+
actor: Player?,
121+
moduleId: String,
122+
rule: ConditionalTaxRule,
123+
context: Map<String, Any?>
124+
): Boolean {
125+
if (rule.condition.isEmpty()) {
126+
return true
127+
}
128+
if (actor == null) {
129+
return false
130+
}
131+
return runCatching {
132+
val builder = ScriptOptions.builder()
133+
.sender(actor)
134+
.set("player", actor)
135+
.set("actor", actor)
136+
.detailError(true)
137+
context.forEach { (key, value) ->
138+
if (value != null) {
139+
builder.set(key, value)
140+
}
141+
}
142+
val result = KetherShell.eval(rule.condition, builder.build()).get(3, TimeUnit.SECONDS)
143+
when (result) {
144+
is Boolean -> result
145+
is Number -> result.toInt() != 0
146+
else -> result?.toString()?.equals("true", true) == true
147+
}
148+
}.getOrElse {
149+
warning("MatrixShop tax rule [$moduleId:${rule.id}] failed to execute: ${it.message ?: it.javaClass.simpleName}")
150+
false
151+
}
152+
}
153+
154+
private fun readStringList(section: ConfigurationSection, vararg paths: String): List<String> {
155+
paths.forEach { path ->
156+
if (section.isList(path)) {
157+
return section.getStringList(path).mapNotNull { it?.trim()?.takeIf(String::isNotBlank) }
158+
}
159+
val single = section.getString(path)?.trim()?.takeIf(String::isNotBlank)
160+
if (single != null) {
161+
return listOf(single)
162+
}
163+
}
164+
return emptyList()
165+
}
166+
}

src/main/kotlin/com/y54895/matrixshop/module/auction/AuctionModels.kt

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package com.y54895.matrixshop.module.auction
22

33
import com.y54895.matrixshop.core.menu.ConfiguredShopMenu
44
import com.y54895.matrixshop.core.menu.MenuDefinition
5+
import com.y54895.matrixshop.core.economy.ConditionalTaxConfig
56
import org.bukkit.inventory.ItemStack
67
import java.util.UUID
78

@@ -60,9 +61,7 @@ data class AuctionSettings(
6061
val depositValue: Double,
6162
val depositRefundOnSell: Boolean,
6263
val depositRefundOnCancel: Boolean,
63-
val taxEnabled: Boolean,
64-
val taxMode: String,
65-
val taxValue: Double,
64+
val tax: ConditionalTaxConfig,
6665
val snipeEnabled: Boolean,
6766
val snipeTriggerSeconds: Int,
6867
val snipeExtendSeconds: Int,

0 commit comments

Comments
 (0)