Blossom Implement (#1606)

* Blossom!

* rename

* delete SpawnBlossomEntry.java

* use MAP

* use List

* use LIST

* use List

* useCondensedResin

* useCondensedResin

* fix build

* enhance

* fix bug

* REMOVE BOSS

* fix condensed resin

* fix condensed resin

* use POSITIVE_INFINITY

* use RewardPreviewData

* fix build

* fix resources

* add BLOSSOM_MONSTER_FIGHTING_VOLUME

* edit monster score

* edit monster score

* fix bug

* fix bug

* improve logic

* fix monsters level

* Deleted comment blocks

* nitpick

* Fix compilation problems

* nitpick

* Refactor + nitpick

* Clean up overall diff to develop

* Clean up other usage of condensed resin

* Clean up overall diff to develop

* Lombokify Scene.java

* Missed an odd getter name

* Unhardcode reward previews

* EDIT NAME

* remove leyline 1

* remove leyline 2

* Update BlossomManager.java

Co-authored-by: AnimeGitB <AnimeGitB@bigblueball.in>
This commit is contained in:
赵怡然 2022-08-21 14:19:59 +08:00 committed by GitHub
parent 957296fa2d
commit abd1e7569e
21 changed files with 670 additions and 261 deletions

View File

@ -113,6 +113,7 @@ public class GameData {
@Getter private static final Int2ObjectMap<TriggerExcelConfigData> triggerExcelConfigDataMap = new Int2ObjectOpenHashMap<>();
@Getter private static final Map<String,ScriptSceneData> scriptSceneDataMap = new HashMap<>();
@Getter private static final Map<Integer, List<Integer>> scenePointsPerScene = new HashMap<>();
@Getter private static final Int2ObjectMap<BlossomRefreshExcelConfigData> blossomRefreshExcelConfigDataMap = new Int2ObjectOpenHashMap<>();
@Getter private static final Int2ObjectMap<OpenStateData> openStateDataMap = new Int2ObjectOpenHashMap<>();
@ -124,7 +125,7 @@ public class GameData {
@Getter private static final List<OpenStateData> openStateList = new ArrayList<>();
public static Int2ObjectMap<?> getMapByResourceDef(Class<?> resourceDefinition) {
Int2ObjectMap<?> map = null;

View File

@ -10,10 +10,13 @@ import emu.grasscutter.Grasscutter;
import emu.grasscutter.data.ResourceLoader.AvatarConfig;
import emu.grasscutter.data.excels.ReliquaryAffixData;
import emu.grasscutter.data.excels.ReliquaryMainPropData;
import emu.grasscutter.game.managers.blossom.BlossomConfig;
import emu.grasscutter.game.world.SpawnDataEntry;
import emu.grasscutter.utils.WeightedList;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import lombok.Getter;
import lombok.Setter;
public class GameDepot {
public static final int[] BLOCK_SIZE = new int[]{50,500};//Scales
@ -22,8 +25,9 @@ public class GameDepot {
private static Int2ObjectMap<List<ReliquaryMainPropData>> relicMainPropDepot = new Int2ObjectOpenHashMap<>();
private static Int2ObjectMap<List<ReliquaryAffixData>> relicAffixDepot = new Int2ObjectOpenHashMap<>();
private static Map<String, AvatarConfig> playerAbilities = new HashMap<>();
private static HashMap<SpawnDataEntry.GridBlockId, ArrayList<SpawnDataEntry>> spawnLists = new HashMap<>();
@Getter @Setter private static Map<String, AvatarConfig> playerAbilities = new HashMap<>();
@Getter private static HashMap<SpawnDataEntry.GridBlockId, ArrayList<SpawnDataEntry>> spawnLists = new HashMap<>();
@Getter @Setter private static BlossomConfig blossomConfig;
public static void load() {
for (ReliquaryMainPropData data : GameData.getReliquaryMainPropDataMap().values()) {
@ -64,19 +68,7 @@ public class GameDepot {
return relicAffixDepot.get(depot);
}
public static HashMap<SpawnDataEntry.GridBlockId, ArrayList<SpawnDataEntry>> getSpawnLists() {
return spawnLists;
}
public static void addSpawnListById(HashMap<SpawnDataEntry.GridBlockId, ArrayList<SpawnDataEntry>> data) {
spawnLists.putAll(data);
}
public static void setPlayerAbilities(Map<String, AvatarConfig> playerAbilities) {
GameDepot.playerAbilities = playerAbilities;
}
public static Map<String, AvatarConfig> getPlayerAbilities() {
return playerAbilities;
}
}

View File

@ -9,6 +9,7 @@ import emu.grasscutter.data.binout.AbilityModifier.AbilityModifierAction;
import emu.grasscutter.data.binout.AbilityModifier.AbilityModifierActionType;
import emu.grasscutter.data.common.PointData;
import emu.grasscutter.data.common.ScenePointConfig;
import emu.grasscutter.game.managers.blossom.BlossomConfig;
import emu.grasscutter.game.quest.QuestEncryptionKey;
import emu.grasscutter.game.world.SpawnDataEntry;
import emu.grasscutter.game.world.SpawnDataEntry.GridBlockId;
@ -75,6 +76,7 @@ public class ResourceLoader {
// Load default home layout
loadHomeworldDefaultSaveData();
loadNpcBornData();
loadBlossomResources();
Grasscutter.getLogger().info(translate("messages.status.resources.finish"));
loadedAll = true;
@ -486,6 +488,12 @@ public class ResourceLoader {
Grasscutter.getLogger().debug("Loaded " + GameData.getSceneNpcBornData().size() + " SceneNpcBornDatas.");
}
@SneakyThrows
private static void loadBlossomResources() {
GameDepot.setBlossomConfig(DataLoader.loadClass("BlossomConfig.json", BlossomConfig.class));
Grasscutter.getLogger().debug("Loaded BlossomConfig.");
}
// BinOutput configs
public static class AvatarConfig {

View File

@ -0,0 +1,46 @@
package emu.grasscutter.data.excels;
import java.util.List;
import emu.grasscutter.data.GameResource;
import emu.grasscutter.data.ResourceType;
import lombok.Getter;
@ResourceType(name = "BlossomRefreshExcelConfigData.json")
public class BlossomRefreshExcelConfigData extends GameResource {
private int id;
// Map details
@Getter private long nameTextMapHash;
@Getter private long descTextMapHash;
@Getter private String icon;
@Getter private String clientShowType; // BLOSSOM_SHOWTYPE_CHALLENGE, BLOSSOM_SHOWTYPE_NPCTALK
// Refresh details
@Getter private String refreshType; // Leyline blossoms, magical ore outcrops
@Getter private int refreshCount; // Number of entries to spawn at refresh (1 for each leyline type for each city, 4 for magical ore for each city)
@Getter private String refreshTime; // Server time-of-day to refresh at
@Getter private RefreshCond[] refreshCondVec; // AR requirements etc.
@Getter private int cityId;
@Getter private int blossomChestId; // 1 for mora, 2 for exp
@Getter private Drop[] dropVec;
// Unknown details
// @Getter private int reviseLevel;
// @Getter private int campUpdateNeedCount; // Always 1 if specified
@Override
public int getId() {
return id;
}
public static class Drop {
@Getter int dropId;
@Getter int previewReward;
}
public static class RefreshCond {
@Getter String type;
@Getter List<Integer> param;
}
}

View File

@ -8,12 +8,12 @@ import emu.grasscutter.game.dungeons.DungeonDrop;
import emu.grasscutter.game.dungeons.DungeonDropEntry;
import emu.grasscutter.game.dungeons.challenge.trigger.ChallengeTrigger;
import emu.grasscutter.game.inventory.GameItem;
import emu.grasscutter.game.inventory.ItemType;
import emu.grasscutter.game.player.Player;
import emu.grasscutter.game.props.ActionReason;
import emu.grasscutter.game.props.WatcherTriggerType;
import emu.grasscutter.game.world.Scene;
import emu.grasscutter.net.proto.GadgetInteractReqOuterClass.GadgetInteractReq;
import emu.grasscutter.net.proto.ResinCostTypeOuterClass;
import emu.grasscutter.scripts.constants.EventType;
import emu.grasscutter.scripts.data.SceneGroup;
import emu.grasscutter.scripts.data.ScriptArgs;
@ -88,11 +88,12 @@ public class DungeonChallenge extends WorldChallenge {
private void settle() {
if (!stage) {
getScene().getDungeonSettleObservers().forEach(o -> o.onDungeonSettle(getScene()));
getScene().getScriptManager().callEvent(EventType.EVENT_DUNGEON_SETTLE,
var scene = this.getScene();
scene.getDungeonSettleListeners().forEach(o -> o.onDungeonSettle(getScene()));
scene.getScriptManager().callEvent(EventType.EVENT_DUNGEON_SETTLE,
new ScriptArgs(this.isSuccess() ? 1 : 0));
// Battle pass trigger
this.getScene().getPlayers().forEach(p -> p.getBattlePassManager().triggerMission(WatcherTriggerType.TRIGGER_FINISH_DUNGEON));
scene.getPlayers().forEach(p -> p.getBattlePassManager().triggerMission(WatcherTriggerType.TRIGGER_FINISH_DUNGEON));
}
}
@ -166,7 +167,7 @@ public class DungeonChallenge extends WorldChallenge {
// Get rewards.
List<GameItem> rewards = new ArrayList<>();
if (request.getIsUseCondenseResin()) {
if (request.getResinCostType() == ResinCostTypeOuterClass.ResinCostType.RESIN_COST_TYPE_CONDENSE) {
// Check if condensed resin is usable here.
// For this, we use the following logic for now:
// The normal resin cost of the dungeon has to be 20.
@ -174,25 +175,15 @@ public class DungeonChallenge extends WorldChallenge {
return;
}
// Make sure the player has condensed resin.
GameItem condensedResin = player.getInventory().getInventoryTab(ItemType.ITEM_MATERIAL).getItemById(220007);
if (condensedResin == null || condensedResin.getCount() <= 0) {
return;
}
// Deduct.
player.getInventory().removeItem(condensedResin, 1);
// Spend the condensed resin and only proceed if the transaction succeeds.
if (!player.getResinManager().useCondensedResin(1)) return;
// Roll rewards.
rewards.addAll(this.rollRewards(true));
}
else {
// If the player used regular resin, try to deduct.
// Stop if insufficient resin.
boolean success = player.getResinManager().useResin(resinCost);
if (!success) {
return;
}
// Spend the resin and only proceed if the transaction succeeds.
if (!player.getResinManager().useResin(resinCost)) return;
// Roll rewards.
rewards.addAll(this.rollRewards(false));

View File

@ -215,12 +215,15 @@ public abstract class GameEntity {
// Invoke entity damage event.
EntityDamageEvent event = new EntityDamageEvent(this, amount, this.getScene().getEntityById(killerId));
event.call(); if (event.isCanceled()) {
event.call();
if (event.isCanceled()) {
return; // If the event is canceled, do not damage the entity.
}
// Add negative HP to the current HP property.
this.addFightProperty(FightProperty.FIGHT_PROP_CUR_HP, -(event.getDamage()));
if(getFightProperty(FightProperty.FIGHT_PROP_CUR_HP) != Float.POSITIVE_INFINITY){
// Add negative HP to the current HP property.
this.addFightProperty(FightProperty.FIGHT_PROP_CUR_HP, -(event.getDamage()));
}
// Check if dead
boolean isDead = false;

View File

@ -2,6 +2,7 @@ package emu.grasscutter.game.entity.gadget;
import emu.grasscutter.Grasscutter;
import emu.grasscutter.game.entity.EntityGadget;
import emu.grasscutter.game.entity.gadget.chest.BossChestInteractHandler;
import emu.grasscutter.game.player.Player;
import emu.grasscutter.game.props.LifeState;
import emu.grasscutter.net.proto.BossChestInfoOuterClass.BossChestInfo;
@ -9,6 +10,7 @@ import emu.grasscutter.net.proto.GadgetInteractReqOuterClass.GadgetInteractReq;
import emu.grasscutter.net.proto.InterOpTypeOuterClass.InterOpType;
import emu.grasscutter.net.proto.InteractTypeOuterClass;
import emu.grasscutter.net.proto.InteractTypeOuterClass.InteractType;
import emu.grasscutter.net.proto.ResinCostTypeOuterClass;
import emu.grasscutter.net.proto.SceneGadgetInfoOuterClass.SceneGadgetInfo;
import emu.grasscutter.scripts.constants.ScriptGadgetState;
import emu.grasscutter.server.packet.send.PacketGadgetInteractRsp;
@ -32,7 +34,13 @@ public class GadgetChest extends GadgetContent {
player.sendPacket(new PacketGadgetInteractRsp(getGadget(), InteractType.INTERACT_TYPE_OPEN_CHEST, InterOpType.INTER_OP_TYPE_START));
return false;
}else {
var success = handler.onInteract(this, player);
boolean success;
if(handler instanceof BossChestInteractHandler bossChestInteractHandler){
success = bossChestInteractHandler.onInteract(this, player,
req.getResinCostType()== ResinCostTypeOuterClass.ResinCostType.RESIN_COST_TYPE_CONDENSE);
}else{
success = handler.onInteract(this, player);
}
if (!success) {
return false;
}

View File

@ -3,31 +3,34 @@ package emu.grasscutter.game.entity.gadget;
import java.util.Arrays;
import emu.grasscutter.game.entity.EntityGadget;
import emu.grasscutter.game.entity.gadget.worktop.WorktopWorktopOptionHandler;
import emu.grasscutter.game.player.Player;
import emu.grasscutter.net.proto.GadgetInteractReqOuterClass.GadgetInteractReq;
import emu.grasscutter.net.proto.SceneGadgetInfoOuterClass.SceneGadgetInfo;
import emu.grasscutter.net.proto.SelectWorktopOptionReqOuterClass.SelectWorktopOptionReq;
import emu.grasscutter.net.proto.WorktopInfoOuterClass.WorktopInfo;
import it.unimi.dsi.fastutil.ints.IntOpenHashSet;
import it.unimi.dsi.fastutil.ints.IntSet;
public class GadgetWorktop extends GadgetContent {
private IntSet worktopOptions;
private WorktopWorktopOptionHandler handler;
public GadgetWorktop(EntityGadget gadget) {
super(gadget);
}
public IntSet getWorktopOptions() {
return worktopOptions;
}
public void addWorktopOptions(int[] options) {
if (this.worktopOptions == null) {
this.worktopOptions = new IntOpenHashSet();
}
Arrays.stream(options).forEach(this.worktopOptions::add);
}
public void removeWorktopOption(int option) {
if (this.worktopOptions == null) {
return;
@ -43,11 +46,20 @@ public class GadgetWorktop extends GadgetContent {
if (this.worktopOptions == null) {
return;
}
WorktopInfo worktop = WorktopInfo.newBuilder()
.addAllOptionList(this.getWorktopOptions())
.build();
gadgetInfo.setWorktop(worktop);
}
public void setOnSelectWorktopOptionEvent(WorktopWorktopOptionHandler handler) {
this.handler = handler;
}
public boolean onSelectWorktopOption(SelectWorktopOptionReq req) {
this.handler.onSelectWorktopOption(this,req.getOptionId());
return false;
}
}

View File

@ -19,6 +19,17 @@ public class BossChestInteractHandler implements ChestInteractHandler{
@Override
public boolean onInteract(GadgetChest chest, Player player) {
return this.onInteract(chest,player,false);
}
public boolean onInteract(GadgetChest chest, Player player,boolean useCondensedResin) {
var blossomRewards = player.getScene().getBlossomManager().onReward(player,chest.getGadget(),useCondensedResin);
if(blossomRewards!=null) {
player.getInventory().addItems(blossomRewards, ActionReason.OpenWorldBossChest);
player.sendPacket(new PacketGadgetAutoPickDropInfoNotify(blossomRewards));
return true;
}
var worldDataManager = chest.getGadget().getScene().getWorld().getServer().getWorldDataSystem();
var monster = chest.getGadget().getMetaGadget().group.monsters.get(chest.getGadget().getMetaGadget().boss_chest.monster_config_id);
var reward = worldDataManager.getRewardByBossId(monster.monster_id);

View File

@ -0,0 +1,6 @@
package emu.grasscutter.game.entity.gadget.worktop;
import emu.grasscutter.game.entity.gadget.GadgetWorktop;
public interface WorktopWorktopOptionHandler {
boolean onSelectWorktopOption(GadgetWorktop gadgetWorktop,int option);
}

View File

@ -52,6 +52,12 @@ public class ResinManager extends BasePlayerManager {
return true;
}
public synchronized boolean useCondensedResin(int amount) {
// Don't deduct if resin disabled.
if (!GAME_OPTIONS.resinOptions.resinUsage) return true;
return this.player.getInventory().payItem(220007, amount);
}
public synchronized void addResin(int amount) {
// Check if resin enabled.
if (!GAME_OPTIONS.resinOptions.resinUsage) {

View File

@ -0,0 +1,135 @@
package emu.grasscutter.game.managers.blossom;
import emu.grasscutter.data.GameData;
import emu.grasscutter.data.excels.MonsterData;
import emu.grasscutter.data.excels.WorldLevelData;
import emu.grasscutter.game.dungeons.challenge.WorldChallenge;
import emu.grasscutter.game.dungeons.challenge.trigger.ChallengeTrigger;
import emu.grasscutter.game.dungeons.challenge.trigger.KillMonsterTrigger;
import emu.grasscutter.game.entity.EntityGadget;
import emu.grasscutter.game.entity.EntityMonster;
import emu.grasscutter.game.props.FightProperty;
import emu.grasscutter.game.world.Scene;
import emu.grasscutter.scripts.data.SceneBossChest;
import emu.grasscutter.scripts.data.SceneGadget;
import emu.grasscutter.scripts.data.SceneGroup;
import emu.grasscutter.utils.Position;
import emu.grasscutter.utils.Utils;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.List;
import java.util.Queue;
public class BlossomActivity {
private final SceneGroup tempSceneGroup;
private final WorldChallenge challenge;
private final EntityGadget gadget;
private EntityGadget chest;
private int step;
private final int goal;
private int generatedCount;
private final int worldLevel;
private boolean pass=false;
private final List<EntityMonster> activeMonsters = new ArrayList<>();
private final Queue<Integer> candidateMonsters = new ArrayDeque<>();
private static final int BLOOMING_GADGET_ID = 70210109;
public BlossomActivity(EntityGadget entityGadget, List<Integer> monsters, int timeout, int worldLevel) {
this.tempSceneGroup = new SceneGroup();
this.tempSceneGroup.id = entityGadget.getId();
this.gadget=entityGadget;
this.step=0;
this.goal = monsters.size();
this.candidateMonsters.addAll(monsters);
this.worldLevel = worldLevel;
ArrayList<ChallengeTrigger> challengeTriggers = new ArrayList<>();
this.challenge = new WorldChallenge(entityGadget.getScene(),
tempSceneGroup,
1,
1,
List.of(goal, timeout),
timeout,
goal, challengeTriggers);
challengeTriggers.add(new KillMonsterTrigger());
//this.challengeTriggers.add(new InTimeTrigger());
}
public WorldChallenge getChallenge(){
return this.challenge;
}
public void setMonsters(List<EntityMonster> monsters) {
this.activeMonsters.clear();
this.activeMonsters.addAll(monsters);
for(EntityMonster monster : monsters){
monster.setGroupId(this.tempSceneGroup.id);
}
}
public int getAliveMonstersCount(){
int count=0;
for(EntityMonster monster: activeMonsters) {
if(monster.isAlive()){
count++;
}
}
return count;
}
public boolean getPass(){
return pass;
}
public void start(){
challenge.start();
}
public void onTick() {
Scene scene = gadget.getScene();
Position pos = gadget.getPosition();
if(getAliveMonstersCount() <= 2){
if(generatedCount<goal){
step++;
WorldLevelData worldLevelData = GameData.getWorldLevelDataMap().get(worldLevel);
int worldLevelOverride = 0;
if (worldLevelData != null) {
worldLevelOverride = worldLevelData.getMonsterLevel();
}
List<EntityMonster> newMonsters = new ArrayList<>();
int willSpawn = Utils.randomRange(3,5);
if(generatedCount+willSpawn>goal){
willSpawn = goal - generatedCount;
}
generatedCount+=willSpawn;
for (int i = 0; i < willSpawn; i++) {
MonsterData monsterData = GameData.getMonsterDataMap().get(candidateMonsters.poll());
int level = scene.getEntityLevel(1, worldLevelOverride);
EntityMonster entity = new EntityMonster(scene, monsterData, pos.nearby2d(40), level);
scene.addEntity(entity);
newMonsters.add(entity);
}
setMonsters(newMonsters);
}else{
if(getAliveMonstersCount() == 0) {
this.pass = true;
this.challenge.done();
}
}
}
}
public EntityGadget getGadget(){
return gadget;
}
public EntityGadget getChest(){
if(chest==null) {
EntityGadget rewardGadget = new EntityGadget(gadget.getScene(), BLOOMING_GADGET_ID, gadget.getPosition());
SceneGadget metaGadget = new SceneGadget();
metaGadget.boss_chest = new SceneBossChest();
metaGadget.boss_chest.resin = 20;
rewardGadget.setFightProperty(FightProperty.FIGHT_PROP_BASE_HP, Float.POSITIVE_INFINITY);
rewardGadget.setFightProperty(FightProperty.FIGHT_PROP_CUR_HP, Float.POSITIVE_INFINITY);
rewardGadget.setFightProperty(FightProperty.FIGHT_PROP_MAX_HP, Float.POSITIVE_INFINITY);
rewardGadget.setMetaGadget(metaGadget);
rewardGadget.buildContent();
chest = rewardGadget;
}
return chest;
}
}

View File

@ -0,0 +1,12 @@
package emu.grasscutter.game.managers.blossom;
import java.util.List;
import java.util.Map;
import lombok.Getter;
public class BlossomConfig {
@Getter private int monsterFightingVolume;
// @Getter private Int2ObjectMap<IntList> monsterIdsPerDifficulty; // Need to wrangle Gson for this
@Getter private Map<Integer, List<Integer>> monsterIdsPerDifficulty;
}

View File

@ -0,0 +1,237 @@
package emu.grasscutter.game.managers.blossom;
import java.util.ArrayList;
import java.util.List;
import emu.grasscutter.Grasscutter;
import emu.grasscutter.data.GameData;
import emu.grasscutter.data.GameDepot;
import emu.grasscutter.data.common.ItemParamData;
import emu.grasscutter.data.excels.RewardPreviewData;
import emu.grasscutter.game.entity.EntityGadget;
import emu.grasscutter.game.entity.GameEntity;
import emu.grasscutter.game.entity.gadget.GadgetWorktop;
import emu.grasscutter.game.inventory.GameItem;
import emu.grasscutter.game.player.Player;
import emu.grasscutter.game.world.Scene;
import emu.grasscutter.game.world.SpawnDataEntry;
import emu.grasscutter.game.world.SpawnDataEntry.SpawnGroupEntry;
import emu.grasscutter.net.proto.BlossomBriefInfoOuterClass;
import emu.grasscutter.net.proto.VisionTypeOuterClass;
import emu.grasscutter.server.packet.send.PacketBlossomBriefInfoNotify;
import emu.grasscutter.utils.Utils;
import it.unimi.dsi.fastutil.ints.Int2ObjectLinkedOpenHashMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.IntArrayList;
import it.unimi.dsi.fastutil.ints.IntList;
public class BlossomManager {
public BlossomManager(Scene scene) {
this.scene = scene;
}
private final Scene scene;
private final List<BlossomActivity> blossomActivities = new ArrayList<>();
private final List<BlossomActivity> activeChests = new ArrayList<>();
private final List<EntityGadget> createdEntity = new ArrayList<>();
private final List<SpawnDataEntry> blossomConsumed = new ArrayList<>();
public void onTick(){
synchronized (blossomActivities){
var it = blossomActivities.iterator();
while(it.hasNext()){
var active = it.next();
active.onTick();
if (active.getPass()) {
EntityGadget chest = active.getChest();
scene.addEntity(chest);
scene.setChallenge(null);
activeChests.add(active);
it.remove();
}
}
}
}
public void recycleGadgetEntity(List<GameEntity> entities){
for(var entity : entities){
if(entity instanceof EntityGadget gadget){
createdEntity.remove(gadget);
}
}
notifyIcon();
}
public void initBlossom(EntityGadget gadget){
if(createdEntity.contains(gadget)){
return;
}
if(blossomConsumed.contains(gadget.getSpawnEntry())){
return;
}
var id = gadget.getGadgetId();
if(BlossomType.valueOf(id)==null){
return;
}
gadget.buildContent();
gadget.setState(204);
int worldLevel = getWorldLevel();
GadgetWorktop gadgetWorktop = ((GadgetWorktop) gadget.getContent());
gadgetWorktop.addWorktopOptions(new int[]{187});
gadgetWorktop.setOnSelectWorktopOptionEvent((GadgetWorktop context, int option) -> {
BlossomActivity activity;
EntityGadget entityGadget = context.getGadget();
synchronized (blossomActivities) {
for (BlossomActivity i : this.blossomActivities) {
if (i.getGadget() == entityGadget) {
return false;
}
}
int volume=0;
IntList monsters = new IntArrayList();
while(true){
var remain = GameDepot.getBlossomConfig().getMonsterFightingVolume() - volume;
if(remain<=0){
break;
}
var rand = Utils.randomRange(1,100);
if(rand>85 && remain>=50){//15% ,generate strong monster
monsters.addAll(getRandomMonstersID(2,1));
volume+=50;
}else if(rand>50 && remain>=20) {//35% ,generate normal monster
monsters.addAll(getRandomMonstersID(1,1));
volume+=20;
}else{//50% ,generate weak monster
monsters.addAll(getRandomMonstersID(0,1));
volume+=10;
}
}
Grasscutter.getLogger().info("Blossom Monsters:"+monsters);
activity = new BlossomActivity(entityGadget, monsters, -1, worldLevel);
blossomActivities.add(activity);
}
entityGadget.updateState(201);
scene.setChallenge(activity.getChallenge());
scene.removeEntity(entityGadget, VisionTypeOuterClass.VisionType.VISION_TYPE_REMOVE);
activity.start();
return true;
});
createdEntity.add(gadget);
notifyIcon();
}
public void notifyIcon() {
final int wl = getWorldLevel();
final int worldLevel = (wl < 0) ? 0 : ((wl > 8) ? 8 : wl);
final int monsterLevel = GameData.getWorldLevelDataMap().get(worldLevel).getMonsterLevel();
List<BlossomBriefInfoOuterClass.BlossomBriefInfo> blossoms = new ArrayList<>();
GameDepot.getSpawnLists().forEach((gridBlockId, spawnDataEntryList) -> {
int sceneId = gridBlockId.getSceneId();
spawnDataEntryList.stream()
.map(SpawnDataEntry::getGroup)
.map(SpawnGroupEntry::getSpawns)
.flatMap(List::stream)
.filter(spawn -> !blossomConsumed.contains(spawn))
.filter(spawn -> BlossomType.valueOf(spawn.getGadgetId()) != null)
.forEach(spawn -> {
var type = BlossomType.valueOf(spawn.getGadgetId());
int previewReward = getPreviewReward(type, worldLevel);
blossoms.add(BlossomBriefInfoOuterClass.BlossomBriefInfo.newBuilder()
.setSceneId(sceneId)
.setPos(spawn.getPos().toProto())
.setResin(20)
.setMonsterLevel(monsterLevel)
.setRewardId(previewReward)
.setCircleCampId(type.getCircleCampId())
.setRefreshId(type.getBlossomChestId()) // TODO: replace when using actual leylines
.build()
);
});
});
scene.broadcastPacket(new PacketBlossomBriefInfoNotify(blossoms));
}
public int getWorldLevel(){
return scene.getWorld().getWorldLevel();
}
private static Integer getPreviewReward(BlossomType type, int worldLevel) {
// TODO: blossoms should be based on their city
if (type == null) {
Grasscutter.getLogger().error("Illegal blossom type {}",type);
return null;
}
int blossomChestId = type.getBlossomChestId();
var dataMap = GameData.getBlossomRefreshExcelConfigDataMap();
for (var data : dataMap.values()) {
if (blossomChestId == data.getBlossomChestId()) {
var dropVecList = data.getDropVec();
if (worldLevel > dropVecList.length) {
Grasscutter.getLogger().error("Illegal world level {}", worldLevel);
return null;
}
return dropVecList[worldLevel].getPreviewReward();
}
}
Grasscutter.getLogger().error("Cannot find blossom type {}",type);
return null;
}
private static RewardPreviewData getRewardList(BlossomType type, int worldLevel) {
Integer previewReward = getPreviewReward(type, worldLevel);
if (previewReward == null) return null;
return GameData.getRewardPreviewDataMap().get((int) previewReward);
}
public List<GameItem> onReward(Player player, EntityGadget chest, boolean useCondensedResin) {
var resinManager = player.getResinManager();
synchronized (activeChests) {
var it = activeChests.iterator();
while (it.hasNext()) {
var activeChest = it.next();
if (activeChest.getChest() == chest) {
boolean pay = useCondensedResin ? resinManager.useCondensedResin(1) : resinManager.useResin(20);
if (pay) {
int worldLevel = getWorldLevel();
List<GameItem> items = new ArrayList<>();
var gadget = activeChest.getGadget();
var type = BlossomType.valueOf(gadget.getGadgetId());
RewardPreviewData blossomRewards = getRewardList(type, worldLevel);
if (blossomRewards == null) {
Grasscutter.getLogger().error("Blossom could not support world level : "+worldLevel);
return null;
}
var rewards = blossomRewards.getPreviewItems();
for (ItemParamData blossomReward : rewards) {
int rewardCount = blossomReward.getCount();
if (useCondensedResin) {
rewardCount += blossomReward.getCount(); // Double!
}
items.add(new GameItem(blossomReward.getItemId(),rewardCount));
}
it.remove();
recycleGadgetEntity(List.of(gadget));
blossomConsumed.add(gadget.getSpawnEntry());
return items;
}
return null;
}
}
}
return null;
}
public static IntList getRandomMonstersID(int difficulty,int count){
IntList result = new IntArrayList();
List<Integer> monsters = GameDepot.getBlossomConfig().getMonsterIdsPerDifficulty().get(difficulty);
for(int i=0; i<count; i++){
result.add((int) monsters.get(Utils.randomRange(0, monsters.size()-1)));
}
return result;
}
}

View File

@ -0,0 +1,37 @@
package emu.grasscutter.game.managers.blossom;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import emu.grasscutter.utils.Utils;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import lombok.Getter;
public enum BlossomType {
GOLD(70360056, 101001001, 1),
BLUE(70360057, 101002003, 2);
@Getter private final int gadgetId;
@Getter private final int circleCampId;
@Getter private final int blossomChestId;
BlossomType(int gadgetId, int circleCampId, int blossomChestId) {
this.gadgetId = gadgetId;
this.circleCampId = circleCampId;
this.blossomChestId = blossomChestId;
}
private static final Int2ObjectMap<BlossomType> map = new Int2ObjectOpenHashMap<>(
Stream.of(values()).collect(Collectors.toMap(x -> x.getGadgetId(), x -> x))
);
public static BlossomType valueOf(int i) {
return map.get(i);
}
public static BlossomType random() {
BlossomType[] values = values();
return values[Utils.randomRange(0, values.length)];
}
}

View File

@ -8,6 +8,8 @@ import emu.grasscutter.data.excels.*;
import emu.grasscutter.game.avatar.Avatar;
import emu.grasscutter.game.dungeons.DungeonSettleListener;
import emu.grasscutter.game.entity.*;
import emu.grasscutter.game.entity.gadget.GadgetWorktop;
import emu.grasscutter.game.managers.blossom.BlossomManager;
import emu.grasscutter.game.player.Player;
import emu.grasscutter.game.player.TeamInfo;
import emu.grasscutter.game.props.FightProperty;
@ -17,6 +19,7 @@ import emu.grasscutter.game.quest.QuestGroupSuite;
import emu.grasscutter.game.dungeons.challenge.WorldChallenge;
import emu.grasscutter.net.packet.BasePacket;
import emu.grasscutter.net.proto.AttackResultOuterClass.AttackResult;
import emu.grasscutter.net.proto.SelectWorktopOptionReqOuterClass;
import emu.grasscutter.net.proto.VisionTypeOuterClass.VisionType;
import emu.grasscutter.scripts.SceneIndexManager;
import emu.grasscutter.scripts.SceneScriptManager;
@ -25,6 +28,8 @@ import emu.grasscutter.scripts.data.SceneGadget;
import emu.grasscutter.scripts.data.SceneGroup;
import emu.grasscutter.server.packet.send.*;
import emu.grasscutter.utils.Position;
import lombok.Getter;
import lombok.Setter;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
@ -32,25 +37,26 @@ import java.util.concurrent.CopyOnWriteArrayList;
import java.util.stream.Collectors;
public class Scene {
private final World world;
private final SceneData sceneData;
private final List<Player> players;
private final Map<Integer, GameEntity> entities;
private final Set<SpawnDataEntry> spawnedEntities;
private final Set<SpawnDataEntry> deadSpawnedEntities;
private final Set<SceneBlock> loadedBlocks;
@Getter private final World world;
@Getter private final SceneData sceneData;
@Getter private final List<Player> players;
@Getter private final Map<Integer, GameEntity> entities;
@Getter private final Set<SpawnDataEntry> spawnedEntities;
@Getter private final Set<SpawnDataEntry> deadSpawnedEntities;
@Getter private final Set<SceneBlock> loadedBlocks;
@Getter private final BlossomManager blossomManager;
private Set<SpawnDataEntry.GridBlockId> loadedGridBlocks;
private boolean dontDestroyWhenEmpty;
@Getter @Setter private boolean dontDestroyWhenEmpty;
private int autoCloseTime;
private int time;
@Getter @Setter private int autoCloseTime;
@Getter private int time;
private SceneScriptManager scriptManager;
private WorldChallenge challenge;
private List<DungeonSettleListener> dungeonSettleListeners;
private DungeonData dungeonData;
private int prevScene; // Id of the previous scene
private int prevScenePoint;
@Getter private SceneScriptManager scriptManager;
@Getter @Setter private WorldChallenge challenge;
@Getter private List<DungeonSettleListener> dungeonSettleListeners;
@Getter private DungeonData dungeonData;
@Getter @Setter private int prevScene; // Id of the previous scene
@Getter @Setter private int prevScenePoint;
private Set<SceneNpcBornEntry> npcBornEntrySet;
public Scene(World world, SceneData sceneData) {
this.world = world;
@ -67,36 +73,21 @@ public class Scene {
this.loadedGridBlocks = new HashSet<>();
this.npcBornEntrySet = ConcurrentHashMap.newKeySet();
this.scriptManager = new SceneScriptManager(this);
this.blossomManager = new BlossomManager(this);
}
public int getId() {
return sceneData.getId();
}
public World getWorld() {
return world;
}
public SceneData getSceneData() {
return this.sceneData;
}
public SceneType getSceneType() {
return getSceneData().getSceneType();
}
public List<Player> getPlayers() {
return players;
}
public int getPlayerCount() {
return this.getPlayers().size();
}
public Map<Integer, GameEntity> getEntities() {
return entities;
}
public GameEntity getEntityById(int id) {
return this.entities.get(id);
}
@ -107,72 +98,11 @@ public class Scene {
.findFirst()
.orElse(null);
}
/**
* @return the autoCloseTime
*/
public int getAutoCloseTime() {
return autoCloseTime;
}
/**
* @param autoCloseTime the autoCloseTime to set
*/
public void setAutoCloseTime(int autoCloseTime) {
this.autoCloseTime = autoCloseTime;
}
public int getTime() {
return time;
}
public void changeTime(int time) {
this.time = time % 1440;
}
public int getPrevScene() {
return prevScene;
}
public void setPrevScene(int prevScene) {
this.prevScene = prevScene;
}
public int getPrevScenePoint() {
return prevScenePoint;
}
public void setPrevScenePoint(int prevPoint) {
this.prevScenePoint = prevPoint;
}
public boolean dontDestroyWhenEmpty() {
return dontDestroyWhenEmpty;
}
public void setDontDestroyWhenEmpty(boolean dontDestroyWhenEmpty) {
this.dontDestroyWhenEmpty = dontDestroyWhenEmpty;
}
public Set<SceneBlock> getLoadedBlocks() {
return loadedBlocks;
}
public Set<SpawnDataEntry> getSpawnedEntities() {
return spawnedEntities;
}
public Set<SpawnDataEntry> getDeadSpawnedEntities() {
return deadSpawnedEntities;
}
public SceneScriptManager getScriptManager() {
return scriptManager;
}
public DungeonData getDungeonData() {
return dungeonData;
}
public void setDungeonData(DungeonData dungeonData) {
if (dungeonData == null || this.dungeonData != null || this.getSceneType() != SceneType.SCENE_DUNGEON || dungeonData.getSceneId() != this.getId()) {
return;
@ -180,14 +110,6 @@ public class Scene {
this.dungeonData = dungeonData;
}
public WorldChallenge getChallenge() {
return challenge;
}
public void setChallenge(WorldChallenge challenge) {
this.challenge = challenge;
}
public void addDungeonSettleObserver(DungeonSettleListener dungeonSettleListener) {
if (dungeonSettleListeners == null) {
dungeonSettleListeners = new ArrayList<>();
@ -195,10 +117,6 @@ public class Scene {
dungeonSettleListeners.add(dungeonSettleListener);
}
public List<DungeonSettleListener> getDungeonSettleObservers() {
return dungeonSettleListeners;
}
public boolean isInScene(GameEntity entity) {
return this.entities.containsKey(entity.getId());
}
@ -241,7 +159,7 @@ public class Scene {
}
// Deregister scene if not in use
if (this.getPlayerCount() <= 0 && !this.dontDestroyWhenEmpty()) {
if (this.getPlayerCount() <= 0 && !this.dontDestroyWhenEmpty) {
this.getWorld().deregisterScene(this);
}
}
@ -434,6 +352,8 @@ public class Scene {
challenge.onCheckTimeOut();
}
blossomManager.onTick();
checkNpcGroup();
}
@ -527,11 +447,12 @@ public class Scene {
}
gadget.buildContent();
gadget.setFightProperty(FightProperty.FIGHT_PROP_BASE_HP, 99999);
gadget.setFightProperty(FightProperty.FIGHT_PROP_CUR_HP, 99999);
gadget.setFightProperty(FightProperty.FIGHT_PROP_MAX_HP, 99999);
gadget.setFightProperty(FightProperty.FIGHT_PROP_BASE_HP, Float.POSITIVE_INFINITY);
gadget.setFightProperty(FightProperty.FIGHT_PROP_CUR_HP, Float.POSITIVE_INFINITY);
gadget.setFightProperty(FightProperty.FIGHT_PROP_MAX_HP, Float.POSITIVE_INFINITY);
entity = gadget;
blossomManager.initBlossom(gadget);
}
if (entity == null) continue;
@ -557,6 +478,7 @@ public class Scene {
if (toRemove.size() > 0) {
toRemove.stream().forEach(this::removeEntityDirectly);
this.broadcastPacket(new PacketSceneEntityDisappearNotify(toRemove, VisionType.VISION_TYPE_REMOVE));
blossomManager.recycleGadgetEntity(toRemove);
}
}
@ -825,4 +747,20 @@ public class Scene {
scriptManager.addGroupSuite(group, suite);
});
}
public void selectWorktopOptionWith(SelectWorktopOptionReqOuterClass.SelectWorktopOptionReq req) {
GameEntity entity = getEntityById(req.getGadgetEntityId());
if (entity == null) {
return;
}
// Handle
if (entity instanceof EntityGadget gadget) {
if (gadget.getContent() instanceof GadgetWorktop worktop) {
boolean shouldDelete = worktop.onSelectWorktopOption(req);
if (shouldDelete) {
entity.getScene().removeEntity(entity, VisionType.VISION_TYPE_REMOVE);
}
}
}
}
}

View File

@ -5,62 +5,20 @@ import java.util.Objects;
import emu.grasscutter.data.GameDepot;
import emu.grasscutter.utils.Position;
import lombok.Getter;
import lombok.Setter;
public class SpawnDataEntry {
private transient SpawnGroupEntry group;
private int monsterId;
private int gadgetId;
private int configId;
private int level;
private int poseId;
private int gatherItemId;
private int gadgetState;
private Position pos;
private Position rot;
public SpawnGroupEntry getGroup() {
return group;
}
public void setGroup(SpawnGroupEntry group) {
this.group = group;
}
public int getMonsterId() {
return monsterId;
}
public int getGadgetId() {
return gadgetId;
}
public int getGadgetState() {
return gadgetState;
}
public int getConfigId() {
return configId;
}
public int getLevel() {
return level;
}
public int getPoseId() {
return poseId;
}
public int getGatherItemId() {
return gatherItemId;
}
public Position getPos() {
return pos;
}
public Position getRot() {
return rot;
}
@Getter @Setter private transient SpawnGroupEntry group;
@Getter private int monsterId;
@Getter private int gadgetId;
@Getter private int configId;
@Getter private int level;
@Getter private int poseId;
@Getter private int gatherItemId;
@Getter private int gadgetState;
@Getter private Position pos;
@Getter private Position rot;
public GridBlockId getBlockId() {
int scale = GridBlockId.getScale(gadgetId);
@ -71,37 +29,17 @@ public class SpawnDataEntry {
}
public static class SpawnGroupEntry {
private int sceneId;
private int groupId;
private int blockId;
private List<SpawnDataEntry> spawns;
public int getSceneId() {
return sceneId;
}
public int getGroupId() {
return groupId;
}
public int getBlockId() {
return blockId;
}
public void setBlockId(int blockId) {
this.blockId = blockId;
}
public List<SpawnDataEntry> getSpawns() {
return spawns;
}
@Getter private int sceneId;
@Getter private int groupId;
@Getter private int blockId;
@Getter @Setter private List<SpawnDataEntry> spawns;
}
public static class GridBlockId {
int sceneId;
int scale;
int x;
int z;
@Getter private int sceneId;
@Getter private int scale;
@Getter private int x;
@Getter private int z;
public GridBlockId(int sceneId, int scale, int x, int z) {
this.sceneId = sceneId;

View File

@ -13,20 +13,20 @@ import emu.grasscutter.server.packet.send.PacketSelectWorktopOptionRsp;
@Opcodes(PacketOpcodes.SelectWorktopOptionReq)
public class HandlerSelectWorktopOptionReq extends PacketHandler {
@Override
public void handle(GameSession session, byte[] header, byte[] payload) throws Exception {
SelectWorktopOptionReq req = SelectWorktopOptionReq.parseFrom(payload);
try {
GameEntity entity = session.getPlayer().getScene().getEntityById(req.getGadgetEntityId());
if (entity == null || !(entity instanceof EntityGadget)) {
if (!(entity instanceof EntityGadget)) {
return;
}
session.getPlayer().getScene().selectWorktopOptionWith(req);
session.getPlayer().getScene().getScriptManager().callEvent(
EventType.EVENT_SELECT_OPTION,
EventType.EVENT_SELECT_OPTION,
new ScriptArgs(entity.getConfigId(), req.getOptionId())
);
} finally {

View File

@ -0,0 +1,13 @@
package emu.grasscutter.server.packet.send;
import emu.grasscutter.net.packet.BasePacket;
import emu.grasscutter.net.packet.PacketOpcodes;
import emu.grasscutter.net.proto.BlossomBriefInfoNotifyOuterClass;
import emu.grasscutter.net.proto.BlossomBriefInfoOuterClass;
public class PacketBlossomBriefInfoNotify extends BasePacket {
public PacketBlossomBriefInfoNotify(Iterable<BlossomBriefInfoOuterClass.BlossomBriefInfo> blossoms) {
super(PacketOpcodes.BlossomBriefInfoNotify);
this.setData(BlossomBriefInfoNotifyOuterClass.BlossomBriefInfoNotify.newBuilder().addAllBriefInfoList(blossoms));
}
}

View File

@ -10,28 +10,28 @@ import emu.grasscutter.net.proto.VectorOuterClass.Vector;
@Entity
public class Position implements Serializable {
private static final long serialVersionUID = -2001232313615923575L;
@SerializedName(value="x", alternate={"_x", "X"})
private float x;
@SerializedName(value="y", alternate={"_y", "Y"})
private float y;
@SerializedName(value="z", alternate={"_z", "Z"})
private float z;
public Position() {
}
public Position(float x, float y) {
set(x, y);
}
public Position(float x, float y, float z) {
set(x, y, z);
}
public Position(String p) {
String[] split = p.split(",");
if (split.length >= 2) {
@ -42,7 +42,7 @@ public class Position implements Serializable {
this.z = Float.parseFloat(split[2]);
}
}
public Position(Vector vector) {
this.set(vector);
}
@ -66,7 +66,7 @@ public class Position implements Serializable {
public void setZ(float z) {
this.z = z;
}
public float getY() {
return y;
}
@ -74,22 +74,22 @@ public class Position implements Serializable {
public void setY(float y) {
this.y = y;
}
public Position set(float x, float y) {
this.x = x;
this.y = y;
return this;
}
// Deep copy
public Position set(Position pos) {
return this.set(pos.getX(), pos.getY(), pos.getZ());
}
public Position set(Vector pos) {
return this.set(pos.getX(), pos.getY(), pos.getZ());
}
public Position set(float x, float y, float z) {
this.x = x;
this.y = y;
@ -103,17 +103,17 @@ public class Position implements Serializable {
this.z += add.getZ();
return this;
}
public Position addX(float d) {
this.x += d;
return this;
}
public Position addY(float d) {
this.y += d;
return this;
}
public Position addZ(float d) {
this.z += d;
return this;
@ -125,7 +125,7 @@ public class Position implements Serializable {
this.z -= sub.getZ();
return this;
}
/** In radians
* */
public Position translate(float dist, float angle) {
@ -149,13 +149,20 @@ public class Position implements Serializable {
double detZ = getZ()-b.getZ();
return Math.sqrt(detX*detX+detY*detY+detZ*detZ);
}
public Position nearby2d(int range){
Position position = clone();
position.z += (float)Utils.randomRange(-range,range)/10;
position.x += (float)Utils.randomRange(-range,range)/10;
return position;
}
public Position translateWithDegrees(float dist, float angle) {
angle = (float) Math.toRadians(angle);
this.x += dist * Math.sin(angle);
this.y += -dist * Math.cos(angle);
return this;
}
@Override
public Position clone() {
return new Position(x, y, z);
@ -165,7 +172,7 @@ public class Position implements Serializable {
public String toString() {
return "(" + this.getX() + ", " + this.getY() + ", " + this.getZ() + ")";
}
public Vector toProto() {
return Vector.newBuilder()
.setX(this.getX())

View File

@ -0,0 +1,8 @@
{
"monsterFightingVolume": 100,
"monsterIdsPerDifficulty": {
"0": [21010101, 20010101, 20010201, 20010202, 20010301, 20010401, 20010403, 20010501, 20010601, 20010604, 20010701, 20010702, 20010801, 20010802, 20010901, 20010902, 20010904, 20011001, 20011101, 20011103, 20011601, 20011701, 20011801, 20011901, 20050201, 20050202, 20050203, 20050301, 20050302, 20050401, 20050402, 20050403, 20050501, 20050502, 20050601, 20050602, 20050603, 20050701, 20050702, 20050703, 20050801, 20050802, 20050901, 21010201, 21010301, 21010401, 21010402, 21010501, 21010502, 21010601, 21010603, 21010701, 21010901, 21010902, 21011001, 21011002, 21011201, 21011301, 21011302, 21011401, 21011403, 21011501, 21011601, 21011602, 20011201, 20011202, 20011301, 20011304, 20011401, 20011501, 20011502, 25010101, 25010102, 25010103, 25010104, 25010105, 25010106, 25010201, 25010203, 25010204, 25010205, 25010206, 25010207, 25010208, 25010701, 25020101, 25020102, 25020201, 25020204, 25030101, 25030102, 25030103, 25030201, 25030301, 25040101, 25040102, 25040103, 25050101, 25050201, 25050301, 25050401, 25050402, 25050501, 25050502, 25060101, 25060102, 25070101, 25070201, 25070202, 21010102, 20010302, 20010402, 20010502, 20010602, 20010703, 20010803, 20010903, 20011002, 20011102, 21010302, 21010702, 21011202, 21011402, 20011203, 20011302, 20011402, 20011503, 21030102, 21030202, 21030302, 21030403, 21030502, 21030602],
"1": [21020101, 21020201, 21020202, 21020301, 21020601, 21020701, 21020703, 21030101, 21030103, 21030201, 21030203, 21030301, 21030303, 21030304, 21030401, 21030402, 21030501, 21030601, 21030603, 25010301, 25010302, 25010401, 25010501, 25010601, 26010101, 26010102, 26010201, 26010301, 21020102, 21020203, 21020702, 21020302, 21020602],
"2": [21020401, 21020501, 21020801, 20020101, 20070101, 22010101, 22010102, 22010103, 22010104, 22010201, 22010202, 22010203, 22010204, 22010301, 22010302, 22010303, 22010304, 22010401, 22010403, 22010404, 24010101, 24010201, 23010101, 23010201, 23010301, 23010401, 23010501, 23010601, 23020101, 23020102, 23030101, 23030102, 23040101, 23040102, 23050101, 20060101, 20060201, 20060301, 20060401, 20060501, 20060601, 21020402, 21020502, 21020802, 22010105, 22010205, 22010305, 22010402]
}
}