From fc42f665a77863ac14b7b6db702245f563c97fc0 Mon Sep 17 00:00:00 2001 From: hamusuke Date: Fri, 8 Sep 2023 12:34:03 +0900 Subject: [PATCH] feat: implement teapot suite (#2344) * feat: implement teapot suite * fix: home animals, check respawn, etc * fix: NPE and cancel summon events * fix: forgot to send eventId also --- .../HomePlantSubFieldDataOuterClass.java | 248 +++++++++--------- .../java/emu/grasscutter/data/GameData.java | 4 + .../data/excels/HomeWorldEventData.java | 25 ++ .../game/entity/EntityHomeAnimal.java | 12 +- .../emu/grasscutter/game/home/GameHome.java | 61 ++++- .../grasscutter/game/home/HomeBlockItem.java | 20 +- .../game/home/HomeModuleManager.java | 204 ++++++++++++++ .../grasscutter/game/home/HomeNPCItem.java | 4 +- .../emu/grasscutter/game/home/HomeScene.java | 26 +- .../grasscutter/game/home/HomeSceneItem.java | 23 +- .../emu/grasscutter/game/home/HomeWorld.java | 50 +++- .../game/home/HomeWorldMPSystem.java | 7 +- .../game/home/suite/HomeSuiteItem.java | 82 ++++++ .../home/suite/event/HomeAvatarEvent.java | 54 ++++ .../suite/event/HomeAvatarRewardEvent.java | 37 +++ .../suite/event/HomeAvatarSummonEvent.java | 36 +++ .../game/home/suite/event/SuiteEventType.java | 19 ++ .../emu/grasscutter/game/player/Player.java | 7 + .../grasscutter/game/props/ActionReason.java | 3 +- .../emu/grasscutter/game/world/World.java | 2 +- .../HandlerHomeAvatarRewardEventGetReq.java | 30 +++ .../recv/HandlerHomeAvatarSummonEventReq.java | 19 ++ .../HandlerHomeAvatarSummonFinishReq.java | 21 ++ .../recv/HandlerHomeChangeEditModeReq.java | 11 +- .../recv/HandlerHomeChangeModuleReq.java | 18 +- .../HandlerHomeEnterEditModeFinishReq.java | 11 +- .../recv/HandlerHomeSceneInitFinishReq.java | 20 +- .../HandlerHomeUpdateArrangementInfoReq.java | 10 +- ...PacketHomeAvatarAllFinishRewardNotify.java | 18 ++ .../PacketHomeAvatarRewardEventGetRsp.java | 26 ++ .../PacketHomeAvatarRewardEventNotify.java | 12 + .../PacketHomeAvatarSummonAllEventNotify.java | 12 + .../send/PacketHomeAvatarSummonEventRsp.java | 22 ++ .../send/PacketHomeAvatarSummonFinishRsp.java | 14 + .../send/PacketPlayerQuitFromHomeNotify.java | 14 + .../java/emu/grasscutter/utils/Either.java | 153 +++++++++++ 36 files changed, 1125 insertions(+), 210 deletions(-) create mode 100644 src/main/java/emu/grasscutter/data/excels/HomeWorldEventData.java create mode 100644 src/main/java/emu/grasscutter/game/home/HomeModuleManager.java create mode 100644 src/main/java/emu/grasscutter/game/home/suite/HomeSuiteItem.java create mode 100644 src/main/java/emu/grasscutter/game/home/suite/event/HomeAvatarEvent.java create mode 100644 src/main/java/emu/grasscutter/game/home/suite/event/HomeAvatarRewardEvent.java create mode 100644 src/main/java/emu/grasscutter/game/home/suite/event/HomeAvatarSummonEvent.java create mode 100644 src/main/java/emu/grasscutter/game/home/suite/event/SuiteEventType.java create mode 100644 src/main/java/emu/grasscutter/server/packet/recv/HandlerHomeAvatarRewardEventGetReq.java create mode 100644 src/main/java/emu/grasscutter/server/packet/recv/HandlerHomeAvatarSummonEventReq.java create mode 100644 src/main/java/emu/grasscutter/server/packet/recv/HandlerHomeAvatarSummonFinishReq.java create mode 100644 src/main/java/emu/grasscutter/server/packet/send/PacketHomeAvatarAllFinishRewardNotify.java create mode 100644 src/main/java/emu/grasscutter/server/packet/send/PacketHomeAvatarRewardEventGetRsp.java create mode 100644 src/main/java/emu/grasscutter/server/packet/send/PacketHomeAvatarRewardEventNotify.java create mode 100644 src/main/java/emu/grasscutter/server/packet/send/PacketHomeAvatarSummonAllEventNotify.java create mode 100644 src/main/java/emu/grasscutter/server/packet/send/PacketHomeAvatarSummonEventRsp.java create mode 100644 src/main/java/emu/grasscutter/server/packet/send/PacketHomeAvatarSummonFinishRsp.java create mode 100644 src/main/java/emu/grasscutter/server/packet/send/PacketPlayerQuitFromHomeNotify.java create mode 100644 src/main/java/emu/grasscutter/utils/Either.java diff --git a/src/generated/main/java/emu/grasscutter/net/proto/HomePlantSubFieldDataOuterClass.java b/src/generated/main/java/emu/grasscutter/net/proto/HomePlantSubFieldDataOuterClass.java index e41b29c80..5e5a599f8 100644 --- a/src/generated/main/java/emu/grasscutter/net/proto/HomePlantSubFieldDataOuterClass.java +++ b/src/generated/main/java/emu/grasscutter/net/proto/HomePlantSubFieldDataOuterClass.java @@ -36,21 +36,21 @@ public final class HomePlantSubFieldDataOuterClass { int getEntityIdList(int index); /** - * .HomePlantFieldStatus CAKDDMKAIMD = 7; - * @return The enum numeric value on the wire for cAKDDMKAIMD. + * .HomePlantFieldStatus status = 7; + * @return The enum numeric value on the wire for status. */ - int getCAKDDMKAIMDValue(); + int getStatusValue(); /** - * .HomePlantFieldStatus CAKDDMKAIMD = 7; - * @return The cAKDDMKAIMD. + * .HomePlantFieldStatus status = 7; + * @return The status. */ - emu.grasscutter.net.proto.HomePlantFieldStatusOuterClass.HomePlantFieldStatus getCAKDDMKAIMD(); + emu.grasscutter.net.proto.HomePlantFieldStatusOuterClass.HomePlantFieldStatus getStatus(); /** - * uint32 JHFNDBIHLNB = 8; - * @return The jHFNDBIHLNB. + * uint32 seed_id = 8; + * @return The seedId. */ - int getJHFNDBIHLNB(); + int getSeedId(); /** * fixed32 end_time = 14; @@ -59,10 +59,10 @@ public final class HomePlantSubFieldDataOuterClass { int getEndTime(); /** - * uint32 KHFGOPCOAGM = 3; - * @return The kHFGOPCOAGM. + * uint32 gather_point_type = 3; + * @return The gatherPointType. */ - int getKHFGOPCOAGM(); + int getGatherPointType(); } /** *
@@ -82,7 +82,7 @@ public final class HomePlantSubFieldDataOuterClass {
     }
     private HomePlantSubFieldData() {
       entityIdList_ = emptyIntList();
-      cAKDDMKAIMD_ = 0;
+      status_ = 0;
     }
 
     @java.lang.Override
@@ -118,7 +118,7 @@ public final class HomePlantSubFieldDataOuterClass {
               break;
             case 24: {
 
-              kHFGOPCOAGM_ = input.readUInt32();
+              gatherPointType_ = input.readUInt32();
               break;
             }
             case 48: {
@@ -145,12 +145,12 @@ public final class HomePlantSubFieldDataOuterClass {
             case 56: {
               int rawValue = input.readEnum();
 
-              cAKDDMKAIMD_ = rawValue;
+              status_ = rawValue;
               break;
             }
             case 64: {
 
-              jHFNDBIHLNB_ = input.readUInt32();
+              seedId_ = input.readUInt32();
               break;
             }
             case 117: {
@@ -221,34 +221,34 @@ public final class HomePlantSubFieldDataOuterClass {
     }
     private int entityIdListMemoizedSerializedSize = -1;
 
-    public static final int CAKDDMKAIMD_FIELD_NUMBER = 7;
-    private int cAKDDMKAIMD_;
+    public static final int STATUS_FIELD_NUMBER = 7;
+    private int status_;
     /**
-     * .HomePlantFieldStatus CAKDDMKAIMD = 7;
-     * @return The enum numeric value on the wire for cAKDDMKAIMD.
+     * .HomePlantFieldStatus status = 7;
+     * @return The enum numeric value on the wire for status.
      */
-    @java.lang.Override public int getCAKDDMKAIMDValue() {
-      return cAKDDMKAIMD_;
+    @java.lang.Override public int getStatusValue() {
+      return status_;
     }
     /**
-     * .HomePlantFieldStatus CAKDDMKAIMD = 7;
-     * @return The cAKDDMKAIMD.
+     * .HomePlantFieldStatus status = 7;
+     * @return The status.
      */
-    @java.lang.Override public emu.grasscutter.net.proto.HomePlantFieldStatusOuterClass.HomePlantFieldStatus getCAKDDMKAIMD() {
+    @java.lang.Override public emu.grasscutter.net.proto.HomePlantFieldStatusOuterClass.HomePlantFieldStatus getStatus() {
       @SuppressWarnings("deprecation")
-      emu.grasscutter.net.proto.HomePlantFieldStatusOuterClass.HomePlantFieldStatus result = emu.grasscutter.net.proto.HomePlantFieldStatusOuterClass.HomePlantFieldStatus.valueOf(cAKDDMKAIMD_);
+      emu.grasscutter.net.proto.HomePlantFieldStatusOuterClass.HomePlantFieldStatus result = emu.grasscutter.net.proto.HomePlantFieldStatusOuterClass.HomePlantFieldStatus.valueOf(status_);
       return result == null ? emu.grasscutter.net.proto.HomePlantFieldStatusOuterClass.HomePlantFieldStatus.UNRECOGNIZED : result;
     }
 
-    public static final int JHFNDBIHLNB_FIELD_NUMBER = 8;
-    private int jHFNDBIHLNB_;
+    public static final int SEED_ID_FIELD_NUMBER = 8;
+    private int seedId_;
     /**
-     * uint32 JHFNDBIHLNB = 8;
-     * @return The jHFNDBIHLNB.
+     * uint32 seed_id = 8;
+     * @return The seedId.
      */
     @java.lang.Override
-    public int getJHFNDBIHLNB() {
-      return jHFNDBIHLNB_;
+    public int getSeedId() {
+      return seedId_;
     }
 
     public static final int END_TIME_FIELD_NUMBER = 14;
@@ -262,15 +262,15 @@ public final class HomePlantSubFieldDataOuterClass {
       return endTime_;
     }
 
-    public static final int KHFGOPCOAGM_FIELD_NUMBER = 3;
-    private int kHFGOPCOAGM_;
+    public static final int GATHER_POINT_TYPE_FIELD_NUMBER = 3;
+    private int gatherPointType_;
     /**
-     * uint32 KHFGOPCOAGM = 3;
-     * @return The kHFGOPCOAGM.
+     * uint32 gather_point_type = 3;
+     * @return The gatherPointType.
      */
     @java.lang.Override
-    public int getKHFGOPCOAGM() {
-      return kHFGOPCOAGM_;
+    public int getGatherPointType() {
+      return gatherPointType_;
     }
 
     private byte memoizedIsInitialized = -1;
@@ -288,8 +288,8 @@ public final class HomePlantSubFieldDataOuterClass {
     public void writeTo(com.google.protobuf.CodedOutputStream output)
                         throws java.io.IOException {
       getSerializedSize();
-      if (kHFGOPCOAGM_ != 0) {
-        output.writeUInt32(3, kHFGOPCOAGM_);
+      if (gatherPointType_ != 0) {
+        output.writeUInt32(3, gatherPointType_);
       }
       if (getEntityIdListList().size() > 0) {
         output.writeUInt32NoTag(50);
@@ -298,11 +298,11 @@ public final class HomePlantSubFieldDataOuterClass {
       for (int i = 0; i < entityIdList_.size(); i++) {
         output.writeUInt32NoTag(entityIdList_.getInt(i));
       }
-      if (cAKDDMKAIMD_ != emu.grasscutter.net.proto.HomePlantFieldStatusOuterClass.HomePlantFieldStatus.HOME_FIELD_STATUE_NONE.getNumber()) {
-        output.writeEnum(7, cAKDDMKAIMD_);
+      if (status_ != emu.grasscutter.net.proto.HomePlantFieldStatusOuterClass.HomePlantFieldStatus.HOME_FIELD_STATUE_NONE.getNumber()) {
+        output.writeEnum(7, status_);
       }
-      if (jHFNDBIHLNB_ != 0) {
-        output.writeUInt32(8, jHFNDBIHLNB_);
+      if (seedId_ != 0) {
+        output.writeUInt32(8, seedId_);
       }
       if (endTime_ != 0) {
         output.writeFixed32(14, endTime_);
@@ -316,9 +316,9 @@ public final class HomePlantSubFieldDataOuterClass {
       if (size != -1) return size;
 
       size = 0;
-      if (kHFGOPCOAGM_ != 0) {
+      if (gatherPointType_ != 0) {
         size += com.google.protobuf.CodedOutputStream
-          .computeUInt32Size(3, kHFGOPCOAGM_);
+          .computeUInt32Size(3, gatherPointType_);
       }
       {
         int dataSize = 0;
@@ -334,13 +334,13 @@ public final class HomePlantSubFieldDataOuterClass {
         }
         entityIdListMemoizedSerializedSize = dataSize;
       }
-      if (cAKDDMKAIMD_ != emu.grasscutter.net.proto.HomePlantFieldStatusOuterClass.HomePlantFieldStatus.HOME_FIELD_STATUE_NONE.getNumber()) {
+      if (status_ != emu.grasscutter.net.proto.HomePlantFieldStatusOuterClass.HomePlantFieldStatus.HOME_FIELD_STATUE_NONE.getNumber()) {
         size += com.google.protobuf.CodedOutputStream
-          .computeEnumSize(7, cAKDDMKAIMD_);
+          .computeEnumSize(7, status_);
       }
-      if (jHFNDBIHLNB_ != 0) {
+      if (seedId_ != 0) {
         size += com.google.protobuf.CodedOutputStream
-          .computeUInt32Size(8, jHFNDBIHLNB_);
+          .computeUInt32Size(8, seedId_);
       }
       if (endTime_ != 0) {
         size += com.google.protobuf.CodedOutputStream
@@ -363,13 +363,13 @@ public final class HomePlantSubFieldDataOuterClass {
 
       if (!getEntityIdListList()
           .equals(other.getEntityIdListList())) return false;
-      if (cAKDDMKAIMD_ != other.cAKDDMKAIMD_) return false;
-      if (getJHFNDBIHLNB()
-          != other.getJHFNDBIHLNB()) return false;
+      if (status_ != other.status_) return false;
+      if (getSeedId()
+          != other.getSeedId()) return false;
       if (getEndTime()
           != other.getEndTime()) return false;
-      if (getKHFGOPCOAGM()
-          != other.getKHFGOPCOAGM()) return false;
+      if (getGatherPointType()
+          != other.getGatherPointType()) return false;
       if (!unknownFields.equals(other.unknownFields)) return false;
       return true;
     }
@@ -385,14 +385,14 @@ public final class HomePlantSubFieldDataOuterClass {
         hash = (37 * hash) + ENTITY_ID_LIST_FIELD_NUMBER;
         hash = (53 * hash) + getEntityIdListList().hashCode();
       }
-      hash = (37 * hash) + CAKDDMKAIMD_FIELD_NUMBER;
-      hash = (53 * hash) + cAKDDMKAIMD_;
-      hash = (37 * hash) + JHFNDBIHLNB_FIELD_NUMBER;
-      hash = (53 * hash) + getJHFNDBIHLNB();
+      hash = (37 * hash) + STATUS_FIELD_NUMBER;
+      hash = (53 * hash) + status_;
+      hash = (37 * hash) + SEED_ID_FIELD_NUMBER;
+      hash = (53 * hash) + getSeedId();
       hash = (37 * hash) + END_TIME_FIELD_NUMBER;
       hash = (53 * hash) + getEndTime();
-      hash = (37 * hash) + KHFGOPCOAGM_FIELD_NUMBER;
-      hash = (53 * hash) + getKHFGOPCOAGM();
+      hash = (37 * hash) + GATHER_POINT_TYPE_FIELD_NUMBER;
+      hash = (53 * hash) + getGatherPointType();
       hash = (29 * hash) + unknownFields.hashCode();
       memoizedHashCode = hash;
       return hash;
@@ -532,13 +532,13 @@ public final class HomePlantSubFieldDataOuterClass {
         super.clear();
         entityIdList_ = emptyIntList();
         bitField0_ = (bitField0_ & ~0x00000001);
-        cAKDDMKAIMD_ = 0;
+        status_ = 0;
 
-        jHFNDBIHLNB_ = 0;
+        seedId_ = 0;
 
         endTime_ = 0;
 
-        kHFGOPCOAGM_ = 0;
+        gatherPointType_ = 0;
 
         return this;
       }
@@ -572,10 +572,10 @@ public final class HomePlantSubFieldDataOuterClass {
           bitField0_ = (bitField0_ & ~0x00000001);
         }
         result.entityIdList_ = entityIdList_;
-        result.cAKDDMKAIMD_ = cAKDDMKAIMD_;
-        result.jHFNDBIHLNB_ = jHFNDBIHLNB_;
+        result.status_ = status_;
+        result.seedId_ = seedId_;
         result.endTime_ = endTime_;
-        result.kHFGOPCOAGM_ = kHFGOPCOAGM_;
+        result.gatherPointType_ = gatherPointType_;
         onBuilt();
         return result;
       }
@@ -634,17 +634,17 @@ public final class HomePlantSubFieldDataOuterClass {
           }
           onChanged();
         }
-        if (other.cAKDDMKAIMD_ != 0) {
-          setCAKDDMKAIMDValue(other.getCAKDDMKAIMDValue());
+        if (other.status_ != 0) {
+          setStatusValue(other.getStatusValue());
         }
-        if (other.getJHFNDBIHLNB() != 0) {
-          setJHFNDBIHLNB(other.getJHFNDBIHLNB());
+        if (other.getSeedId() != 0) {
+          setSeedId(other.getSeedId());
         }
         if (other.getEndTime() != 0) {
           setEndTime(other.getEndTime());
         }
-        if (other.getKHFGOPCOAGM() != 0) {
-          setKHFGOPCOAGM(other.getKHFGOPCOAGM());
+        if (other.getGatherPointType() != 0) {
+          setGatherPointType(other.getGatherPointType());
         }
         this.mergeUnknownFields(other.unknownFields);
         onChanged();
@@ -755,87 +755,87 @@ public final class HomePlantSubFieldDataOuterClass {
         return this;
       }
 
-      private int cAKDDMKAIMD_ = 0;
+      private int status_ = 0;
       /**
-       * .HomePlantFieldStatus CAKDDMKAIMD = 7;
-       * @return The enum numeric value on the wire for cAKDDMKAIMD.
+       * .HomePlantFieldStatus status = 7;
+       * @return The enum numeric value on the wire for status.
        */
-      @java.lang.Override public int getCAKDDMKAIMDValue() {
-        return cAKDDMKAIMD_;
+      @java.lang.Override public int getStatusValue() {
+        return status_;
       }
       /**
-       * .HomePlantFieldStatus CAKDDMKAIMD = 7;
-       * @param value The enum numeric value on the wire for cAKDDMKAIMD to set.
+       * .HomePlantFieldStatus status = 7;
+       * @param value The enum numeric value on the wire for status to set.
        * @return This builder for chaining.
        */
-      public Builder setCAKDDMKAIMDValue(int value) {
+      public Builder setStatusValue(int value) {
         
-        cAKDDMKAIMD_ = value;
+        status_ = value;
         onChanged();
         return this;
       }
       /**
-       * .HomePlantFieldStatus CAKDDMKAIMD = 7;
-       * @return The cAKDDMKAIMD.
+       * .HomePlantFieldStatus status = 7;
+       * @return The status.
        */
       @java.lang.Override
-      public emu.grasscutter.net.proto.HomePlantFieldStatusOuterClass.HomePlantFieldStatus getCAKDDMKAIMD() {
+      public emu.grasscutter.net.proto.HomePlantFieldStatusOuterClass.HomePlantFieldStatus getStatus() {
         @SuppressWarnings("deprecation")
-        emu.grasscutter.net.proto.HomePlantFieldStatusOuterClass.HomePlantFieldStatus result = emu.grasscutter.net.proto.HomePlantFieldStatusOuterClass.HomePlantFieldStatus.valueOf(cAKDDMKAIMD_);
+        emu.grasscutter.net.proto.HomePlantFieldStatusOuterClass.HomePlantFieldStatus result = emu.grasscutter.net.proto.HomePlantFieldStatusOuterClass.HomePlantFieldStatus.valueOf(status_);
         return result == null ? emu.grasscutter.net.proto.HomePlantFieldStatusOuterClass.HomePlantFieldStatus.UNRECOGNIZED : result;
       }
       /**
-       * .HomePlantFieldStatus CAKDDMKAIMD = 7;
-       * @param value The cAKDDMKAIMD to set.
+       * .HomePlantFieldStatus status = 7;
+       * @param value The status to set.
        * @return This builder for chaining.
        */
-      public Builder setCAKDDMKAIMD(emu.grasscutter.net.proto.HomePlantFieldStatusOuterClass.HomePlantFieldStatus value) {
+      public Builder setStatus(emu.grasscutter.net.proto.HomePlantFieldStatusOuterClass.HomePlantFieldStatus value) {
         if (value == null) {
           throw new NullPointerException();
         }
         
-        cAKDDMKAIMD_ = value.getNumber();
+        status_ = value.getNumber();
         onChanged();
         return this;
       }
       /**
-       * .HomePlantFieldStatus CAKDDMKAIMD = 7;
+       * .HomePlantFieldStatus status = 7;
        * @return This builder for chaining.
        */
-      public Builder clearCAKDDMKAIMD() {
+      public Builder clearStatus() {
         
-        cAKDDMKAIMD_ = 0;
+        status_ = 0;
         onChanged();
         return this;
       }
 
-      private int jHFNDBIHLNB_ ;
+      private int seedId_ ;
       /**
-       * uint32 JHFNDBIHLNB = 8;
-       * @return The jHFNDBIHLNB.
+       * uint32 seed_id = 8;
+       * @return The seedId.
        */
       @java.lang.Override
-      public int getJHFNDBIHLNB() {
-        return jHFNDBIHLNB_;
+      public int getSeedId() {
+        return seedId_;
       }
       /**
-       * uint32 JHFNDBIHLNB = 8;
-       * @param value The jHFNDBIHLNB to set.
+       * uint32 seed_id = 8;
+       * @param value The seedId to set.
        * @return This builder for chaining.
        */
-      public Builder setJHFNDBIHLNB(int value) {
+      public Builder setSeedId(int value) {
         
-        jHFNDBIHLNB_ = value;
+        seedId_ = value;
         onChanged();
         return this;
       }
       /**
-       * uint32 JHFNDBIHLNB = 8;
+       * uint32 seed_id = 8;
        * @return This builder for chaining.
        */
-      public Builder clearJHFNDBIHLNB() {
+      public Builder clearSeedId() {
         
-        jHFNDBIHLNB_ = 0;
+        seedId_ = 0;
         onChanged();
         return this;
       }
@@ -871,33 +871,33 @@ public final class HomePlantSubFieldDataOuterClass {
         return this;
       }
 
-      private int kHFGOPCOAGM_ ;
+      private int gatherPointType_ ;
       /**
-       * uint32 KHFGOPCOAGM = 3;
-       * @return The kHFGOPCOAGM.
+       * uint32 gather_point_type = 3;
+       * @return The gatherPointType.
        */
       @java.lang.Override
-      public int getKHFGOPCOAGM() {
-        return kHFGOPCOAGM_;
+      public int getGatherPointType() {
+        return gatherPointType_;
       }
       /**
-       * uint32 KHFGOPCOAGM = 3;
-       * @param value The kHFGOPCOAGM to set.
+       * uint32 gather_point_type = 3;
+       * @param value The gatherPointType to set.
        * @return This builder for chaining.
        */
-      public Builder setKHFGOPCOAGM(int value) {
+      public Builder setGatherPointType(int value) {
         
-        kHFGOPCOAGM_ = value;
+        gatherPointType_ = value;
         onChanged();
         return this;
       }
       /**
-       * uint32 KHFGOPCOAGM = 3;
+       * uint32 gather_point_type = 3;
        * @return This builder for chaining.
        */
-      public Builder clearKHFGOPCOAGM() {
+      public Builder clearGatherPointType() {
         
-        kHFGOPCOAGM_ = 0;
+        gatherPointType_ = 0;
         onChanged();
         return this;
       }
@@ -969,12 +969,12 @@ public final class HomePlantSubFieldDataOuterClass {
   static {
     java.lang.String[] descriptorData = {
       "\n\033HomePlantSubFieldData.proto\032\032HomePlant" +
-      "FieldStatus.proto\"\227\001\n\025HomePlantSubFieldD" +
-      "ata\022\026\n\016entity_id_list\030\006 \003(\r\022*\n\013CAKDDMKAI" +
-      "MD\030\007 \001(\0162\025.HomePlantFieldStatus\022\023\n\013JHFND" +
-      "BIHLNB\030\010 \001(\r\022\020\n\010end_time\030\016 \001(\007\022\023\n\013KHFGOP" +
-      "COAGM\030\003 \001(\rB\033\n\031emu.grasscutter.net.proto" +
-      "b\006proto3"
+      "FieldStatus.proto\"\224\001\n\025HomePlantSubFieldD" +
+      "ata\022\026\n\016entity_id_list\030\006 \003(\r\022%\n\006status\030\007 " +
+      "\001(\0162\025.HomePlantFieldStatus\022\017\n\007seed_id\030\010 " +
+      "\001(\r\022\020\n\010end_time\030\016 \001(\007\022\031\n\021gather_point_ty" +
+      "pe\030\003 \001(\rB\033\n\031emu.grasscutter.net.protob\006p" +
+      "roto3"
     };
     descriptor = com.google.protobuf.Descriptors.FileDescriptor
       .internalBuildGeneratedFileFrom(descriptorData,
@@ -986,7 +986,7 @@ public final class HomePlantSubFieldDataOuterClass {
     internal_static_HomePlantSubFieldData_fieldAccessorTable = new
       com.google.protobuf.GeneratedMessageV3.FieldAccessorTable(
         internal_static_HomePlantSubFieldData_descriptor,
-        new java.lang.String[] { "EntityIdList", "CAKDDMKAIMD", "JHFNDBIHLNB", "EndTime", "KHFGOPCOAGM", });
+        new java.lang.String[] { "EntityIdList", "Status", "SeedId", "EndTime", "GatherPointType", });
     emu.grasscutter.net.proto.HomePlantFieldStatusOuterClass.getDescriptor();
   }
 
diff --git a/src/main/java/emu/grasscutter/data/GameData.java b/src/main/java/emu/grasscutter/data/GameData.java
index 400cade1e..24b9cbb7e 100644
--- a/src/main/java/emu/grasscutter/data/GameData.java
+++ b/src/main/java/emu/grasscutter/data/GameData.java
@@ -286,6 +286,10 @@ public final class GameData {
     private static final Int2ObjectMap homeWorldBgmDataMap =
             new Int2ObjectOpenHashMap<>();
 
+    @Getter
+    private static final Int2ObjectMap homeWorldEventDataMap =
+            new Int2ObjectOpenHashMap<>();
+
     @Getter
     private static final Int2ObjectMap homeWorldLevelDataMap =
             new Int2ObjectOpenHashMap<>();
diff --git a/src/main/java/emu/grasscutter/data/excels/HomeWorldEventData.java b/src/main/java/emu/grasscutter/data/excels/HomeWorldEventData.java
new file mode 100644
index 000000000..d5977c817
--- /dev/null
+++ b/src/main/java/emu/grasscutter/data/excels/HomeWorldEventData.java
@@ -0,0 +1,25 @@
+package emu.grasscutter.data.excels;
+
+import com.google.gson.annotations.SerializedName;
+import emu.grasscutter.data.GameResource;
+import emu.grasscutter.data.ResourceType;
+import emu.grasscutter.game.home.suite.event.SuiteEventType;
+import lombok.AccessLevel;
+import lombok.Getter;
+import lombok.experimental.FieldDefaults;
+
+@ResourceType(name = "HomeWorldEventExcelConfigData.json")
+@FieldDefaults(level = AccessLevel.PRIVATE)
+@Getter
+public class HomeWorldEventData extends GameResource {
+    @SerializedName(value = "id", alternate = {"BBEIIPEFDPE"})
+    int id;
+    @SerializedName(value = "eventType", alternate = {"JOCKIMECHDP"})
+    SuiteEventType eventType;
+    int avatarID;
+    @SerializedName(value = "talkId", alternate = {"IGNJAICDFPD"})
+    int talkId;
+    int rewardID;
+    @SerializedName(value = "suiteId", alternate = {"FEHOKMJPOED"})
+    int suiteId;
+}
diff --git a/src/main/java/emu/grasscutter/game/entity/EntityHomeAnimal.java b/src/main/java/emu/grasscutter/game/entity/EntityHomeAnimal.java
index 426849aa9..c6652d967 100644
--- a/src/main/java/emu/grasscutter/game/entity/EntityHomeAnimal.java
+++ b/src/main/java/emu/grasscutter/game/entity/EntityHomeAnimal.java
@@ -10,12 +10,14 @@ import emu.grasscutter.server.packet.send.PacketSceneEntityAppearNotify;
 import emu.grasscutter.server.packet.send.PacketSceneEntityDisappearNotify;
 import lombok.Getter;
 
+import java.util.concurrent.atomic.AtomicBoolean;
+
 public class EntityHomeAnimal extends EntityMonster implements Rebornable {
     private int rebornCDTickCount;
     private final Position rebornPos;
     @Getter private final int rebirth;
     @Getter private final int rebirthCD;
-    private boolean disappeared;
+    private final AtomicBoolean disappeared = new AtomicBoolean();
 
     public EntityHomeAnimal(Scene scene, HomeWorldAnimalData data, Position pos) {
         super(scene, GameData.getMonsterDataMap().get(data.getMonsterID()), pos, 1);
@@ -60,13 +62,13 @@ public class EntityHomeAnimal extends EntityMonster implements Rebornable {
                         new PacketSceneEntityDisappearNotify(
                                 this, VisionTypeOuterClass.VisionType.VISION_TYPE_REMOVE));
         this.rebornCDTickCount = this.getRebornCD();
-        this.disappeared = true;
+        this.disappeared.set(true);
     }
 
     @Override
     public void reborn() {
-        if (this.disappeared) {
-            this.disappeared = false;
+        if (this.disappeared.get()) {
+            this.disappeared.set(false);
             this.getPosition().set(this.getRebornPos());
             this.getScene().broadcastPacket(new PacketSceneEntityAppearNotify(this));
         }
@@ -74,6 +76,6 @@ public class EntityHomeAnimal extends EntityMonster implements Rebornable {
 
     @Override
     public boolean isInCD() {
-        return this.disappeared;
+        return this.disappeared.get();
     }
 }
diff --git a/src/main/java/emu/grasscutter/game/home/GameHome.java b/src/main/java/emu/grasscutter/game/home/GameHome.java
index 52f58e3f6..fd5bcc893 100644
--- a/src/main/java/emu/grasscutter/game/home/GameHome.java
+++ b/src/main/java/emu/grasscutter/game/home/GameHome.java
@@ -12,15 +12,17 @@ import emu.grasscutter.game.props.SceneType;
 import emu.grasscutter.net.proto.HomeAvatarTalkFinishInfoOuterClass;
 import emu.grasscutter.server.packet.send.*;
 import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
+import lombok.AccessLevel;
+import lombok.Builder;
+import lombok.Data;
+import lombok.experimental.FieldDefaults;
+
 import java.time.ZonedDateTime;
 import java.time.temporal.ChronoUnit;
 import java.util.*;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.stream.Collectors;
-import lombok.AccessLevel;
-import lombok.Builder;
-import lombok.Data;
-import lombok.experimental.FieldDefaults;
+import java.util.stream.Stream;
 
 @Entity(value = "homes", useDiscriminator = false)
 @Data
@@ -55,6 +57,7 @@ public class GameHome {
     Set unlockedHomeBgmList;
     int enterHomeOption;
     Map> finishedTalkIdMap;
+    Set finishedRewardEventIdSet;
 
     public static GameHome getByUid(Integer uid) {
         var home = DatabaseHelper.getHomeByUid(uid);
@@ -62,7 +65,9 @@ public class GameHome {
             home = GameHome.create(uid);
         }
 
+        home.reassignIfNull();
         home.fixMainHouseIfOld();
+        home.syncHomeAvatarCostume();
 
         return home;
     }
@@ -79,9 +84,19 @@ public class GameHome {
                 .mainHouseMap(new ConcurrentHashMap<>())
                 .unlockedHomeBgmList(new HashSet<>())
                 .finishedTalkIdMap(new HashMap<>())
+                .finishedRewardEventIdSet(new HashSet<>())
                 .build();
     }
 
+    // avoid NPE caused by database remover.
+    private void reassignIfNull() {
+        this.getSceneMap().values().stream()
+            .map(HomeSceneItem::getBlockItems)
+            .map(Map::values)
+            .flatMap(Collection::stream)
+            .forEach(HomeBlockItem::reassignIfNull);
+    }
+
     // Data fixer.
     private void fixMainHouseIfOld() {
         if (this.getMainHouseMap() == null) {
@@ -97,6 +112,18 @@ public class GameHome {
         this.save();
     }
 
+    private void syncHomeAvatarCostume() {
+        Stream.of(this.sceneMap, this.mainHouseMap)
+            .map(ConcurrentHashMap::values)
+            .flatMap(Collection::stream)
+            .map(HomeSceneItem::getBlockItems)
+            .map(Map::values)
+            .flatMap(Collection::stream)
+            .map(HomeBlockItem::getDeployNPCList)
+            .flatMap(Collection::stream)
+            .forEach(npc -> npc.setCostumeId(this.getPlayer().getCostumeFrom(npc.getAvatarId())));
+    }
+
     public void save() {
         DatabaseHelper.saveHome(this);
     }
@@ -113,12 +140,12 @@ public class GameHome {
                     if (defaultItem != null) {
                         Grasscutter.getLogger()
                                 .info("Set player {} home {} to initial setting", ownerUid, sceneId);
-                        return HomeSceneItem.parseFrom(defaultItem, sceneId);
                     } else {
                         // Realm res missing bricks account, use default realm data to allow main house
                         defaultItem = GameData.getHomeworldDefaultSaveData().get(2001);
-                        return HomeSceneItem.parseFrom(defaultItem, sceneId);
                     }
+
+                    return HomeSceneItem.parseFrom(defaultItem, sceneId);
                 });
     }
 
@@ -149,6 +176,8 @@ public class GameHome {
         this.getMainHouseMap().remove(outdoor); // delete main house in current scene.
         this.getMainHouseItem(outdoor); // put new main house with default arrangement.
         this.save();
+
+        this.getPlayer().getCurHomeWorld().getModuleManager().refreshMainHouse();
     }
 
     public void onOwnerLogin(Player player) {
@@ -160,6 +189,8 @@ public class GameHome {
         player.getSession().send(new PacketHomeMarkPointNotify(player));
         player.getSession().send(new PacketHomeAvatarTalkFinishInfoNotify(player));
         player.getSession().send(new PacketHomeAllUnlockedBgmIdListNotify(player));
+        player.getSession().send(new PacketHomeAvatarRewardEventNotify(player));
+        player.getSession().send(new PacketHomeAvatarAllFinishRewardNotify(player));
         checkAccumulatedResources(player);
         player.getSession().send(new PacketHomeResourceNotify(player));
     }
@@ -226,6 +257,20 @@ public class GameHome {
                 .toList();
     }
 
+    public boolean onClaimAvatarRewards(int eventId) {
+        if (this.finishedRewardEventIdSet == null) {
+            this.finishedRewardEventIdSet = new HashSet<>();
+        }
+
+        var success = this.finishedRewardEventIdSet.add(eventId);
+        this.save();
+        return success;
+    }
+
+    public boolean isRewardEventFinished(int eventId) {
+        return this.finishedRewardEventIdSet != null && this.finishedRewardEventIdSet.contains(eventId);
+    }
+
     public boolean addUnlockedHomeBgm(int homeBgmId) {
         if (!getUnlockedHomeBgmList().add(homeBgmId)) return false;
 
@@ -404,7 +449,7 @@ public class GameHome {
                 newCoin = storedCoin + owedCoin;
             }
             // Ensure max is not exceeded
-            storedCoin = (maxCoin >= newCoin) ? newCoin : maxCoin;
+            storedCoin = Math.min(maxCoin, newCoin);
         }
 
         // Update fetter exp
@@ -416,7 +461,7 @@ public class GameHome {
                 newFetter = storedFetterExp + owedFetter;
             }
             // Ensure max is not exceeded
-            storedFetterExp = (maxFetter >= newFetter) ? newFetter : maxFetter;
+            storedFetterExp = Math.min(maxFetter, newFetter);
         }
 
         save();
diff --git a/src/main/java/emu/grasscutter/game/home/HomeBlockItem.java b/src/main/java/emu/grasscutter/game/home/HomeBlockItem.java
index dbd080d46..3fa916e78 100644
--- a/src/main/java/emu/grasscutter/game/home/HomeBlockItem.java
+++ b/src/main/java/emu/grasscutter/game/home/HomeBlockItem.java
@@ -2,6 +2,8 @@ package emu.grasscutter.game.home;
 
 import dev.morphia.annotations.*;
 import emu.grasscutter.data.binout.HomeworldDefaultSaveData;
+import emu.grasscutter.game.home.suite.HomeSuiteItem;
+import emu.grasscutter.game.player.Player;
 import emu.grasscutter.net.proto.HomeBlockArrangementInfoOuterClass.HomeBlockArrangementInfo;
 import java.util.*;
 import java.util.stream.Stream;
@@ -19,6 +21,7 @@ public class HomeBlockItem {
     List persistentFurnitureList;
     List deployAnimalList;
     List deployNPCList;
+    List suiteList;
 
     public static HomeBlockItem parseFrom(HomeworldDefaultSaveData.HomeBlock homeBlock) {
         // create from default setting
@@ -37,10 +40,11 @@ public class HomeBlockItem {
                                         .toList())
                 .deployAnimalList(List.of())
                 .deployNPCList(List.of())
+                .suiteList(List.of())
                 .build();
     }
 
-    public void update(HomeBlockArrangementInfo homeBlockArrangementInfo) {
+    public void update(HomeBlockArrangementInfo homeBlockArrangementInfo, Player owner) {
         this.blockId = homeBlockArrangementInfo.getBlockId();
 
         this.deployFurnitureList =
@@ -60,8 +64,12 @@ public class HomeBlockItem {
 
         this.deployNPCList =
                 homeBlockArrangementInfo.getDeployNpcListList().stream()
-                        .map(HomeNPCItem::parseFrom)
+                        .map(homeNpcData -> HomeNPCItem.parseFrom(homeNpcData, owner))
                         .toList();
+
+        this.suiteList = homeBlockArrangementInfo.getFurnitureSuiteListList().stream()
+            .map(HomeSuiteItem::parseFrom)
+            .toList();
     }
 
     public int calComfort() {
@@ -81,15 +89,16 @@ public class HomeBlockItem {
         this.persistentFurnitureList.forEach(f -> proto.addPersistentFurnitureList(f.toProto()));
         this.deployAnimalList.forEach(f -> proto.addDeployAnimalList(f.toProto()));
         this.deployNPCList.forEach(f -> proto.addDeployNpcList(f.toProto()));
+        this.suiteList.forEach(f -> proto.addFurnitureSuiteList(f.toProto()));
 
         return proto.build();
     }
 
-    // TODO add more types (farm field and suite)
+    // TODO implement farm field.
     public List getMarkPointProtoFactories() {
         this.reassignIfNull();
 
-        return Stream.of(this.deployFurnitureList, this.persistentFurnitureList, this.deployNPCList)
+        return Stream.of(this.deployFurnitureList, this.persistentFurnitureList, this.deployNPCList, this.suiteList)
                 .flatMap(Collection::stream)
                 .toList();
     }
@@ -107,5 +116,8 @@ public class HomeBlockItem {
         if (this.deployNPCList == null) {
             this.deployNPCList = List.of();
         }
+        if (this.suiteList == null) {
+            this.suiteList = List.of();
+        }
     }
 }
diff --git a/src/main/java/emu/grasscutter/game/home/HomeModuleManager.java b/src/main/java/emu/grasscutter/game/home/HomeModuleManager.java
new file mode 100644
index 000000000..d49f5650f
--- /dev/null
+++ b/src/main/java/emu/grasscutter/game/home/HomeModuleManager.java
@@ -0,0 +1,204 @@
+package emu.grasscutter.game.home;
+
+import com.github.davidmoten.guavamini.Lists;
+import emu.grasscutter.Grasscutter;
+import emu.grasscutter.game.home.suite.event.HomeAvatarRewardEvent;
+import emu.grasscutter.game.home.suite.event.HomeAvatarSummonEvent;
+import emu.grasscutter.game.home.suite.event.SuiteEventType;
+import emu.grasscutter.game.inventory.GameItem;
+import emu.grasscutter.game.player.Player;
+import emu.grasscutter.net.proto.HomeAvatarRewardEventNotifyOuterClass;
+import emu.grasscutter.net.proto.HomeAvatarSummonAllEventNotifyOuterClass;
+import emu.grasscutter.net.proto.RetcodeOuterClass;
+import emu.grasscutter.server.packet.send.PacketHomeAvatarSummonAllEventNotify;
+import emu.grasscutter.utils.Either;
+import lombok.AccessLevel;
+import lombok.Getter;
+import lombok.experimental.FieldDefaults;
+
+import java.util.*;
+import java.util.stream.Stream;
+
+@Getter
+@FieldDefaults(level = AccessLevel.PRIVATE)
+public class HomeModuleManager {
+    final Player homeOwner;
+    final HomeWorld homeWorld;
+    final GameHome home;
+    final int moduleId;
+    final HomeScene outdoor;
+    HomeScene indoor;
+    final List rewardEvents;
+    final List summonEvents;
+
+    public HomeModuleManager(HomeWorld homeWorld) {
+        this.homeOwner = homeWorld.getHost();
+        this.homeWorld = homeWorld;
+        this.home = homeWorld.getHome();
+        this.moduleId = this.homeOwner.getCurrentRealmId();
+        this.outdoor = homeWorld.getSceneById(homeWorld.getActiveOutdoorSceneId());
+        this.refreshMainHouse();
+        this.rewardEvents = Lists.newArrayList();
+        this.summonEvents = Collections.synchronizedList(Lists.newArrayList());
+    }
+
+    public void tick() {
+        this.outdoor.onTick();
+        this.indoor.onTick();
+        this.summonEvents.removeIf(HomeAvatarSummonEvent::isTimeOver);
+    }
+
+    public void refreshMainHouse() {
+        this.indoor = this.homeWorld.getSceneById(this.homeWorld.getActiveIndoorSceneId());
+    }
+
+    public void onUpdateArrangement() {
+        this.fireAllAvatarRewardEvent();
+        this.cancelSummonEventIfAvatarLeave();
+    }
+
+    private void fireAllAvatarRewardEvent() {
+        this.rewardEvents.clear();
+        var allBlockItems = Stream.of(this.getOutdoorSceneItem(), this.getIndoorSceneItem())
+            .map(HomeSceneItem::getBlockItems)
+            .map(Map::values)
+            .flatMap(Collection::stream)
+            .toList();
+
+        var suites = allBlockItems.stream()
+            .map(HomeBlockItem::getSuiteList)
+            .flatMap(Collection::stream)
+            .distinct()
+            .toList();
+
+        allBlockItems.stream()
+            .map(HomeBlockItem::getDeployNPCList)
+            .flatMap(Collection::stream)
+            .forEach(avatar -> {
+                suites.forEach(suite -> {
+                    var data = SuiteEventType.HOME_AVATAR_REWARD_EVENT.getEventDataFrom(avatar.getAvatarId(), suite.getSuiteId());
+                    if (data == null || this.home.isRewardEventFinished(data.getId())) {
+                        return;
+                    }
+
+                    this.rewardEvents.add(new HomeAvatarRewardEvent(homeOwner, data.getId(), data.getRewardID(), data.getAvatarID(), data.getSuiteId(), suite.getGuid()));
+                });
+            });
+
+        if (this.summonEvents != null) {
+            var suiteIdList = this.rewardEvents.stream().map(HomeAvatarRewardEvent::getSuiteId).toList();
+            this.summonEvents.removeIf(event -> suiteIdList.contains(event.getSuiteId()));
+        }
+    }
+
+    private void cancelSummonEventIfAvatarLeave() {
+        var avatars = Stream.of(this.getOutdoorSceneItem(), this.getIndoorSceneItem())
+            .map(HomeSceneItem::getBlockItems)
+            .map(Map::values)
+            .flatMap(Collection::stream)
+            .map(HomeBlockItem::getDeployNPCList)
+            .flatMap(Collection::stream)
+            .map(HomeNPCItem::getAvatarId)
+            .toList();
+
+        this.summonEvents.removeIf(event -> !avatars.contains(event.getAvatarId()));
+    }
+
+    public Either, Integer> claimAvatarRewards(int eventId) {
+        if (this.rewardEvents.isEmpty()) {
+            return Either.right(RetcodeOuterClass.Retcode.RET_FAIL_VALUE);
+        }
+
+        var event = this.rewardEvents.remove(0);
+        if (event.getEventId() != eventId) {
+            return Either.right(RetcodeOuterClass.Retcode.RET_FAIL_VALUE);
+        }
+
+        if (!this.homeOwner.getHome().onClaimAvatarRewards(eventId)) {
+            return Either.right(RetcodeOuterClass.Retcode.RET_FAIL_VALUE);
+        }
+
+        return Either.left(event.giveRewards());
+    }
+
+    public Either fireAvatarSummonEvent(Player owner, int avatarId, int guid, int suiteId) {
+        var targetSuite = ((HomeScene) owner.getScene()).getSceneItem().getBlockItems().values().stream()
+            .map(HomeBlockItem::getSuiteList)
+            .flatMap(Collection::stream)
+            .filter(suite -> suite.getGuid() == guid)
+            .findFirst()
+            .orElse(null);
+
+        if (this.isInRewardEvent(avatarId)) {
+            return Either.right(RetcodeOuterClass.Retcode.RET_DUPLICATE_AVATAR_VALUE);
+        }
+
+        if (this.rewardEvents.stream().anyMatch(event -> event.getGuid() == guid)) {
+            return Either.right(RetcodeOuterClass.Retcode.RET_HOME_FURNITURE_GUID_ERROR_VALUE);
+        }
+
+        this.summonEvents.removeIf(event -> event.getGuid() == guid || event.getAvatarId() == avatarId);
+
+        if (targetSuite == null) {
+            return Either.right(RetcodeOuterClass.Retcode.RET_HOME_CLIENT_PARAM_INVALID_VALUE);
+        }
+
+        var eventData = SuiteEventType.HOME_AVATAR_SUMMON_EVENT.getEventDataFrom(avatarId, suiteId);
+        if (eventData == null) {
+            return Either.right(RetcodeOuterClass.Retcode.RET_HOME_CLIENT_PARAM_INVALID_VALUE);
+        }
+
+        var event = new HomeAvatarSummonEvent(owner, eventData.getId(), eventData.getRewardID(), avatarId, suiteId, guid);
+        this.summonEvents.add(event);
+        owner.sendPacket(new PacketHomeAvatarSummonAllEventNotify(owner));
+        return Either.left(event);
+    }
+
+    public void onFinishSummonEvent(int eventId) {
+        this.summonEvents.removeIf(event -> event.getEventId() == eventId);
+    }
+
+    public HomeAvatarRewardEventNotifyOuterClass.HomeAvatarRewardEventNotify toRewardEventProto() {
+        var notify = HomeAvatarRewardEventNotifyOuterClass.HomeAvatarRewardEventNotify.newBuilder();
+        if (!this.rewardEvents.isEmpty()) {
+            notify.setRewardEvent(this.rewardEvents.get(0).toProto()).setIsEventTrigger(true);
+
+            notify.addAllPendingList(this.rewardEvents.subList(1, this.rewardEvents.size()).stream()
+                .map(HomeAvatarRewardEvent::toProto)
+                .toList());
+        }
+
+        return notify.build();
+    }
+
+    public HomeAvatarSummonAllEventNotifyOuterClass.HomeAvatarSummonAllEventNotify toSummonEventProto() {
+        return HomeAvatarSummonAllEventNotifyOuterClass.HomeAvatarSummonAllEventNotify.newBuilder()
+            .addAllSummonEventList(this.summonEvents.stream()
+                .map(HomeAvatarSummonEvent::toProto)
+                .toList())
+            .build();
+    }
+
+    public boolean isInRewardEvent(int avatarId) {
+        return this.rewardEvents.stream().anyMatch(e -> e.getAvatarId() == avatarId);
+    }
+
+    public HomeSceneItem getOutdoorSceneItem() {
+        return this.outdoor.getSceneItem();
+    }
+
+    public HomeSceneItem getIndoorSceneItem() {
+        return this.indoor.getSceneItem();
+    }
+
+    public void onSetModule() {
+        this.outdoor.addEntities(this.getOutdoorSceneItem().getAnimals(this.outdoor));
+        this.indoor.addEntities(this.getIndoorSceneItem().getAnimals(this.indoor));
+        this.fireAllAvatarRewardEvent();
+    }
+
+    public void onRemovedModule() {
+        this.outdoor.getEntities().clear();
+        this.indoor.getEntities().clear();
+    }
+}
diff --git a/src/main/java/emu/grasscutter/game/home/HomeNPCItem.java b/src/main/java/emu/grasscutter/game/home/HomeNPCItem.java
index fbd2ad377..7999fcd39 100644
--- a/src/main/java/emu/grasscutter/game/home/HomeNPCItem.java
+++ b/src/main/java/emu/grasscutter/game/home/HomeNPCItem.java
@@ -2,6 +2,7 @@ package emu.grasscutter.game.home;
 
 import dev.morphia.annotations.Entity;
 import emu.grasscutter.data.GameData;
+import emu.grasscutter.game.player.Player;
 import emu.grasscutter.game.world.Position;
 import emu.grasscutter.net.proto.HomeMarkPointFurnitureDataOuterClass;
 import emu.grasscutter.net.proto.HomeMarkPointNPCDataOuterClass;
@@ -23,11 +24,12 @@ public class HomeNPCItem implements HomeMarkPointProtoFactory {
     Position spawnRot;
     int costumeId;
 
-    public static HomeNPCItem parseFrom(HomeNpcDataOuterClass.HomeNpcData homeNpcData) {
+    public static HomeNPCItem parseFrom(HomeNpcDataOuterClass.HomeNpcData homeNpcData, Player owner) {
         return HomeNPCItem.of()
                 .avatarId(homeNpcData.getAvatarId())
                 .spawnPos(new Position(homeNpcData.getSpawnPos()))
                 .spawnRot(new Position(homeNpcData.getSpawnRot()))
+                .costumeId(owner.getCostumeFrom(homeNpcData.getAvatarId()))
                 .build();
     }
 
diff --git a/src/main/java/emu/grasscutter/game/home/HomeScene.java b/src/main/java/emu/grasscutter/game/home/HomeScene.java
index b07f798b0..3c6df4b22 100644
--- a/src/main/java/emu/grasscutter/game/home/HomeScene.java
+++ b/src/main/java/emu/grasscutter/game/home/HomeScene.java
@@ -1,9 +1,12 @@
 package emu.grasscutter.game.home;
 
 import emu.grasscutter.data.excels.scene.SceneData;
+import emu.grasscutter.game.entity.EntityHomeAnimal;
 import emu.grasscutter.game.entity.GameEntity;
+import emu.grasscutter.game.entity.Rebornable;
 import emu.grasscutter.game.player.Player;
 import emu.grasscutter.game.world.Scene;
+import emu.grasscutter.net.proto.VisionTypeOuterClass;
 import emu.grasscutter.server.packet.send.PacketSceneTimeNotify;
 
 public class HomeScene extends Scene {
@@ -40,10 +43,31 @@ public class HomeScene extends Scene {
                 .forEach(gameEntity -> gameEntity.onTick(this.getSceneTimeSeconds()));
 
         this.finishLoading();
-        this.checkPlayerRespawn();
         if (this.tickCount++ % 10 == 0) this.broadcastPacket(new PacketSceneTimeNotify(this));
     }
 
+    public void onEnterEditModeFinish() {
+        this.removeEntities(
+            this.getEntities().values().stream()
+                .filter(gameEntity -> gameEntity instanceof EntityHomeAnimal)
+                .toList(),
+            VisionTypeOuterClass.VisionType.VISION_TYPE_REMOVE);
+    }
+
+    public void onLeaveEditMode() {
+        this.addEntities(this.getSceneItem().getAnimals(this));
+    }
+
+    @Override
+    public void killEntity(GameEntity target, int attackerId) {
+        if (target instanceof Rebornable rebornable) {
+            rebornable.onAiKillSelf(); // Teapot animals will not die. They will revive!
+            return;
+        }
+
+        super.killEntity(target, attackerId);
+    }
+
     @Override
     public void checkNpcGroup() {}
 
diff --git a/src/main/java/emu/grasscutter/game/home/HomeSceneItem.java b/src/main/java/emu/grasscutter/game/home/HomeSceneItem.java
index 7449f029f..21da833dd 100644
--- a/src/main/java/emu/grasscutter/game/home/HomeSceneItem.java
+++ b/src/main/java/emu/grasscutter/game/home/HomeSceneItem.java
@@ -6,17 +6,20 @@ import emu.grasscutter.Grasscutter;
 import emu.grasscutter.data.GameData;
 import emu.grasscutter.data.binout.HomeworldDefaultSaveData;
 import emu.grasscutter.game.entity.EntityHomeAnimal;
+import emu.grasscutter.game.player.Player;
 import emu.grasscutter.game.world.Position;
 import emu.grasscutter.game.world.Scene;
 import emu.grasscutter.net.proto.HomeSceneArrangementInfoOuterClass.HomeSceneArrangementInfo;
+import lombok.AccessLevel;
+import lombok.Builder;
+import lombok.Data;
+import lombok.experimental.FieldDefaults;
+
+import javax.annotation.Nullable;
 import java.util.Collection;
 import java.util.List;
 import java.util.Map;
-import java.util.concurrent.atomic.AtomicReference;
 import java.util.stream.Collectors;
-import javax.annotation.Nullable;
-import lombok.*;
-import lombok.experimental.FieldDefaults;
 
 @Entity
 @Data
@@ -49,14 +52,14 @@ public class HomeSceneItem {
                 .build();
     }
 
-    public void update(HomeSceneArrangementInfo arrangementInfo) {
+    public void update(HomeSceneArrangementInfo arrangementInfo, Player owner) {
         for (var blockItem : arrangementInfo.getBlockArrangementInfoListList()) {
             var block = this.blockItems.get(blockItem.getBlockId());
             if (block == null) {
                 Grasscutter.getLogger().warn("Could not found the Home Block {}", blockItem.getBlockId());
                 continue;
             }
-            block.update(blockItem);
+            block.update(blockItem, owner);
             this.blockItems.put(blockItem.getBlockId(), block);
         }
 
@@ -84,17 +87,13 @@ public class HomeSceneItem {
     }
 
     @Nullable public Position getTeleportPointPos(int guid) {
-        var pos = new AtomicReference();
-
-        this.getBlockItems().values().stream()
+        return this.getBlockItems().values().stream()
                 .map(HomeBlockItem::getDeployFurnitureList)
                 .flatMap(Collection::stream)
                 .filter(homeFurnitureItem -> homeFurnitureItem.getGuid() == guid)
                 .map(HomeFurnitureItem::getSpawnPos)
                 .findFirst()
-                .ifPresent(pos::set);
-
-        return pos.get();
+                .orElse(null);
     }
 
     public List getAnimals(Scene scene) {
diff --git a/src/main/java/emu/grasscutter/game/home/HomeWorld.java b/src/main/java/emu/grasscutter/game/home/HomeWorld.java
index f2ea9d3b6..8f871657b 100644
--- a/src/main/java/emu/grasscutter/game/home/HomeWorld.java
+++ b/src/main/java/emu/grasscutter/game/home/HomeWorld.java
@@ -8,33 +8,57 @@ import emu.grasscutter.game.world.World;
 import emu.grasscutter.net.packet.BasePacket;
 import emu.grasscutter.net.proto.ChatInfoOuterClass;
 import emu.grasscutter.server.game.GameServer;
-import emu.grasscutter.server.packet.send.*;
-import java.util.List;
+import emu.grasscutter.server.packet.send.PacketDelTeamEntityNotify;
+import emu.grasscutter.server.packet.send.PacketPlayerChatNotify;
+import emu.grasscutter.server.packet.send.PacketPlayerGameTimeNotify;
 import lombok.Getter;
 
+import java.util.List;
+import java.util.function.Consumer;
+
 public class HomeWorld extends World {
     @Getter private final GameHome home;
+    @Getter private HomeModuleManager moduleManager;
 
     public HomeWorld(GameServer server, Player owner) {
         super(server, owner);
 
         this.home = owner.isOnline() ? owner.getHome() : GameHome.getByUid(owner.getUid());
         server.registerHomeWorld(this);
+        this.refreshModuleManager();
     }
 
     @Override
-    public void registerScene(Scene scene) {
-        this.addAnimalsToScene((HomeScene) scene);
-        super.registerScene(scene);
+    public boolean onTick() {
+        this.moduleManager.tick();
+
+        if (this.getTickCount() % 10 == 0) {
+            this.getPlayers().forEach(p -> p.sendPacket(new PacketPlayerGameTimeNotify(p)));
+        }
+
+        if (this.isInHome(this.getHost()) && this.getTickCount() % 60 == 0) {
+            this.getHost().updatePlayerGameTime(this.getCurrentWorldTime());
+        }
+
+        this.tickCount++;
+        return false;
     }
 
-    @Override
-    public void deregisterScene(Scene scene) {
-        super.deregisterScene(scene);
+    public void refreshModuleManager() {
+        if (this.moduleManager != null) {
+            this.moduleManager.onRemovedModule();
+        }
+
+        this.moduleManager = new HomeModuleManager(this);
+        this.moduleManager.onSetModule();
     }
 
-    private void addAnimalsToScene(HomeScene scene) {
-        scene.getSceneItem().getAnimals(scene).forEach(scene::addEntity);
+    public int getActiveOutdoorSceneId() {
+        return this.getHost().getCurrentRealmId() + 2000;
+    }
+
+    public int getActiveIndoorSceneId() {
+        return this.getSceneById(this.getActiveOutdoorSceneId()).getSceneItem().getRoomSceneId();
     }
 
     @Override
@@ -188,6 +212,12 @@ public class HomeWorld extends World {
         return this.getPlayers().contains(player);
     }
 
+    public void ifHost(Player hostOrGuest, Consumer ifHost) {
+        if (this.getHost().equals(hostOrGuest)) {
+            ifHost.accept(hostOrGuest);
+        }
+    }
+
     public void sendPacketToHostIfOnline(BasePacket basePacket) {
         if (this.getHost().isOnline()) {
             this.getHost().sendPacket(basePacket);
diff --git a/src/main/java/emu/grasscutter/game/home/HomeWorldMPSystem.java b/src/main/java/emu/grasscutter/game/home/HomeWorldMPSystem.java
index aecacb21c..f0da0bf86 100644
--- a/src/main/java/emu/grasscutter/game/home/HomeWorldMPSystem.java
+++ b/src/main/java/emu/grasscutter/game/home/HomeWorldMPSystem.java
@@ -5,10 +5,7 @@ import emu.grasscutter.game.props.EnterReason;
 import emu.grasscutter.game.world.Position;
 import emu.grasscutter.game.world.World;
 import emu.grasscutter.game.world.data.TeleportProperties;
-import emu.grasscutter.net.proto.EnterTypeOuterClass;
-import emu.grasscutter.net.proto.OtherPlayerEnterHomeNotifyOuterClass;
-import emu.grasscutter.net.proto.PlayerApplyEnterHomeResultNotifyOuterClass;
-import emu.grasscutter.net.proto.RetcodeOuterClass;
+import emu.grasscutter.net.proto.*;
 import emu.grasscutter.server.event.player.PlayerEnterHomeEvent;
 import emu.grasscutter.server.event.player.PlayerLeaveHomeEvent;
 import emu.grasscutter.server.event.player.PlayerTeleportEvent;
@@ -215,6 +212,7 @@ public class HomeWorldMPSystem extends BaseGameSystem {
         player.setCurHomeWorld(myHome);
         myHome.getHome().onOwnerLogin(player);
 
+        player.sendPacket(new PacketPlayerQuitFromHomeNotify(PlayerQuitFromHomeNotifyOuterClass.PlayerQuitFromHomeNotify.QuitReason.BACK_TO_MY_WORLD));
         player.sendPacket(
                 new PacketPlayerEnterSceneNotify(
                         player,
@@ -263,6 +261,7 @@ public class HomeWorldMPSystem extends BaseGameSystem {
         victim.setCurHomeWorld(myHome);
         myHome.getHome().onOwnerLogin(victim);
 
+        victim.sendPacket(new PacketPlayerQuitFromHomeNotify(PlayerQuitFromHomeNotifyOuterClass.PlayerQuitFromHomeNotify.QuitReason.KICK_BY_HOST));
         victim.sendPacket(
                 new PacketPlayerEnterSceneNotify(
                         victim,
diff --git a/src/main/java/emu/grasscutter/game/home/suite/HomeSuiteItem.java b/src/main/java/emu/grasscutter/game/home/suite/HomeSuiteItem.java
new file mode 100644
index 000000000..263240452
--- /dev/null
+++ b/src/main/java/emu/grasscutter/game/home/suite/HomeSuiteItem.java
@@ -0,0 +1,82 @@
+package emu.grasscutter.game.home.suite;
+
+import dev.morphia.annotations.Entity;
+import emu.grasscutter.game.home.HomeMarkPointProtoFactory;
+import emu.grasscutter.game.home.SpecialFurnitureType;
+import emu.grasscutter.game.world.Position;
+import emu.grasscutter.net.proto.HomeFurnitureSuiteDataOuterClass;
+import emu.grasscutter.net.proto.HomeMarkPointFurnitureDataOuterClass;
+import emu.grasscutter.net.proto.HomeMarkPointSuiteDataOuterClass;
+import lombok.AccessLevel;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.experimental.FieldDefaults;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.List;
+import java.util.Objects;
+
+@Entity
+@Builder(builderMethodName = "of")
+@Getter
+@FieldDefaults(level = AccessLevel.PRIVATE)
+public class HomeSuiteItem implements HomeMarkPointProtoFactory {
+    public static final int SUITE_FURNITURE_ID = 377101;
+    int guid;
+    int suiteId;
+    Position pos;
+    List includedFurnitureIndexList;
+    boolean isAllowSummon;
+
+    public static HomeSuiteItem parseFrom(HomeFurnitureSuiteDataOuterClass.HomeFurnitureSuiteData data) {
+        return HomeSuiteItem.of()
+            .guid(data.getGuid())
+            .suiteId(data.getSuiteId())
+            .pos(new Position(data.getSpawnPos()))
+            .includedFurnitureIndexList(data.getIncludedFurnitureIndexListList())
+            .isAllowSummon(data.getIsAllowSummon())
+            .build();
+    }
+
+    public HomeFurnitureSuiteDataOuterClass.HomeFurnitureSuiteData toProto() {
+        return HomeFurnitureSuiteDataOuterClass.HomeFurnitureSuiteData.newBuilder()
+            .setSuiteId(this.suiteId)
+            .setGuid(this.guid)
+            .setIsAllowSummon(this.isAllowSummon)
+            .addAllIncludedFurnitureIndexList(this.includedFurnitureIndexList)
+            .setSpawnPos(this.pos.toProto())
+            .build();
+    }
+
+    @Nullable
+    @Override
+    public HomeMarkPointFurnitureDataOuterClass.HomeMarkPointFurnitureData toMarkPointProto() {
+        return HomeMarkPointFurnitureDataOuterClass.HomeMarkPointFurnitureData.newBuilder()
+            .setFurnitureId(SUITE_FURNITURE_ID)
+            .setPos(this.pos.toProto())
+            .setFurnitureType(this.getType().getValue())
+            .setGuid(this.guid)
+            .setSuiteData(HomeMarkPointSuiteDataOuterClass.HomeMarkPointSuiteData.newBuilder()
+                .setSuiteId(this.suiteId)
+                .build())
+            .build();
+    }
+
+    @Override
+    public SpecialFurnitureType getType() {
+        return SpecialFurnitureType.FurnitureSuite;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        HomeSuiteItem that = (HomeSuiteItem) o;
+        return suiteId == that.suiteId;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(suiteId);
+    }
+}
diff --git a/src/main/java/emu/grasscutter/game/home/suite/event/HomeAvatarEvent.java b/src/main/java/emu/grasscutter/game/home/suite/event/HomeAvatarEvent.java
new file mode 100644
index 000000000..60a4707f9
--- /dev/null
+++ b/src/main/java/emu/grasscutter/game/home/suite/event/HomeAvatarEvent.java
@@ -0,0 +1,54 @@
+package emu.grasscutter.game.home.suite.event;
+
+import emu.grasscutter.game.inventory.GameItem;
+import emu.grasscutter.game.player.Player;
+import emu.grasscutter.utils.Utils;
+import lombok.AccessLevel;
+import lombok.Getter;
+import lombok.experimental.FieldDefaults;
+
+import java.util.List;
+import java.util.Objects;
+
+@Getter
+@FieldDefaults(level = AccessLevel.PRIVATE)
+public abstract class HomeAvatarEvent {
+    final Player homeOwner;
+    final int eventId;
+    final int rewardId;
+    final int avatarId;
+    final int suiteId;
+    final int guid;
+    final int randomPos;
+
+    public HomeAvatarEvent(Player homeOwner, int eventId, int rewardId, int avatarId, int suiteId, int guid) {
+        this.homeOwner = homeOwner;
+        this.eventId = eventId;
+        this.rewardId = rewardId;
+        this.avatarId = avatarId;
+        this.suiteId = suiteId;
+        this.guid = guid;
+        this.randomPos = this.generateRandomPos();
+    }
+
+    public int generateRandomPos() {
+        return Utils.randomRange(1, 97);
+    }
+
+    public List giveRewards() {
+        return List.of();
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        HomeAvatarEvent that = (HomeAvatarEvent) o;
+        return eventId == that.eventId;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(eventId);
+    }
+}
diff --git a/src/main/java/emu/grasscutter/game/home/suite/event/HomeAvatarRewardEvent.java b/src/main/java/emu/grasscutter/game/home/suite/event/HomeAvatarRewardEvent.java
new file mode 100644
index 000000000..651212142
--- /dev/null
+++ b/src/main/java/emu/grasscutter/game/home/suite/event/HomeAvatarRewardEvent.java
@@ -0,0 +1,37 @@
+package emu.grasscutter.game.home.suite.event;
+
+import emu.grasscutter.data.GameData;
+import emu.grasscutter.game.inventory.GameItem;
+import emu.grasscutter.game.player.Player;
+import emu.grasscutter.game.props.ActionReason;
+import emu.grasscutter.net.proto.HomeAvatarRewardEventInfoOuterClass;
+
+import java.util.List;
+
+public class HomeAvatarRewardEvent extends HomeAvatarEvent {
+    public HomeAvatarRewardEvent(Player homeOwner, int eventId, int rewardId, int avatarId, int suiteId, int guid) {
+        super(homeOwner, eventId, rewardId, avatarId, suiteId, guid);
+    }
+
+    public HomeAvatarRewardEventInfoOuterClass.HomeAvatarRewardEventInfo toProto() {
+        return HomeAvatarRewardEventInfoOuterClass.HomeAvatarRewardEventInfo.newBuilder()
+            .setAvatarId(this.getAvatarId())
+            .setEventId(this.getEventId())
+            .setGuid(this.getGuid())
+            .setSuiteId(this.getSuiteId())
+            .setRandomPosition(this.getRandomPos())
+            .build();
+    }
+
+    @Override
+    public List giveRewards() {
+        var data = GameData.getRewardDataMap().get(this.getRewardId());
+        if (data == null) {
+            return List.of();
+        }
+
+        var rewards = data.getRewardItemList().stream().map(GameItem::new).toList();
+        this.getHomeOwner().getInventory().addItems(rewards, ActionReason.HomeAvatarEventReward);
+        return rewards;
+    }
+}
diff --git a/src/main/java/emu/grasscutter/game/home/suite/event/HomeAvatarSummonEvent.java b/src/main/java/emu/grasscutter/game/home/suite/event/HomeAvatarSummonEvent.java
new file mode 100644
index 000000000..961cfe7ab
--- /dev/null
+++ b/src/main/java/emu/grasscutter/game/home/suite/event/HomeAvatarSummonEvent.java
@@ -0,0 +1,36 @@
+package emu.grasscutter.game.home.suite.event;
+
+import emu.grasscutter.game.player.Player;
+import emu.grasscutter.net.proto.HomeAvatarSummonEventInfoOuterClass;
+import emu.grasscutter.utils.Utils;
+import lombok.AccessLevel;
+import lombok.Getter;
+import lombok.experimental.FieldDefaults;
+
+@Getter
+@FieldDefaults(level = AccessLevel.PRIVATE)
+public class HomeAvatarSummonEvent extends HomeAvatarEvent {
+    public static final int TIME_LIMIT_SECS = 240;
+    final int eventOverTime;
+
+    public HomeAvatarSummonEvent(Player homeOwner, int eventId, int rewardId, int avatarId, int suiteId, int guid) {
+        super(homeOwner, eventId, rewardId, avatarId, suiteId, guid);
+
+        this.eventOverTime = Utils.getCurrentSeconds() + TIME_LIMIT_SECS;
+    }
+
+    public HomeAvatarSummonEventInfoOuterClass.HomeAvatarSummonEventInfo toProto() {
+        return HomeAvatarSummonEventInfoOuterClass.HomeAvatarSummonEventInfo.newBuilder()
+            .setAvatarId(this.getAvatarId())
+            .setEventId(this.getEventId())
+            .setGuid(this.getGuid())
+            .setSuitId(this.getSuiteId())
+            .setRandomPosition(this.getRandomPos())
+            .setEventOverTime(this.eventOverTime)
+            .build();
+    }
+
+    public boolean isTimeOver() {
+        return Utils.getCurrentSeconds() > this.eventOverTime;
+    }
+}
diff --git a/src/main/java/emu/grasscutter/game/home/suite/event/SuiteEventType.java b/src/main/java/emu/grasscutter/game/home/suite/event/SuiteEventType.java
new file mode 100644
index 000000000..8c501f462
--- /dev/null
+++ b/src/main/java/emu/grasscutter/game/home/suite/event/SuiteEventType.java
@@ -0,0 +1,19 @@
+package emu.grasscutter.game.home.suite.event;
+
+import emu.grasscutter.data.GameData;
+import emu.grasscutter.data.excels.HomeWorldEventData;
+
+import javax.annotation.Nullable;
+
+public enum SuiteEventType {
+    HOME_AVATAR_REWARD_EVENT,
+    HOME_AVATAR_SUMMON_EVENT;
+
+    @Nullable
+    public HomeWorldEventData getEventDataFrom(int avatarId, int suiteId) {
+        return GameData.getHomeWorldEventDataMap().values().stream()
+            .filter(data -> data.getEventType() == this && data.getAvatarID() == avatarId && data.getSuiteId() == suiteId)
+            .findFirst()
+            .orElse(null);
+    }
+}
diff --git a/src/main/java/emu/grasscutter/game/player/Player.java b/src/main/java/emu/grasscutter/game/player/Player.java
index d4b242d31..5adc13b80 100644
--- a/src/main/java/emu/grasscutter/game/player/Player.java
+++ b/src/main/java/emu/grasscutter/game/player/Player.java
@@ -950,6 +950,13 @@ public class Player implements PlayerHook, FieldFetch {
         this.sendPacket(new PacketAvatarGainCostumeNotify(costumeId));
     }
 
+    public int getCostumeFrom(int avatarId) {
+        var avatars = this.getAvatars();
+        avatars.loadFromDatabase();
+        var avatar = avatars.getAvatarById(avatarId);
+        return avatar == null ? 0 : avatar.getCostume();
+    }
+
     public void addPersonalLine(int personalLineId) {
         this.getPersonalLineList().add(personalLineId);
         session.getPlayer().getQuestManager().queueEvent(QuestCond.QUEST_COND_PERSONAL_LINE_UNLOCK, personalLineId);
diff --git a/src/main/java/emu/grasscutter/game/props/ActionReason.java b/src/main/java/emu/grasscutter/game/props/ActionReason.java
index 76a2aed58..57867ed35 100644
--- a/src/main/java/emu/grasscutter/game/props/ActionReason.java
+++ b/src/main/java/emu/grasscutter/game/props/ActionReason.java
@@ -177,7 +177,8 @@ public enum ActionReason {
     ChannellerSlabLoopDungeonFirstPassReward(1090),
     ChannellerSlabLoopDungeonScoreReward(1091),
     HomeLimitedShopBuy(1092),
-    HomeCoinCollect(1093);
+    HomeCoinCollect(1093),
+    HomeAvatarEventReward(1100);
 
     private static final Int2ObjectMap map = new Int2ObjectOpenHashMap<>();
     private static final Map stringMap = new HashMap<>();
diff --git a/src/main/java/emu/grasscutter/game/world/World.java b/src/main/java/emu/grasscutter/game/world/World.java
index 3b9e01ed0..085e60fe7 100644
--- a/src/main/java/emu/grasscutter/game/world/World.java
+++ b/src/main/java/emu/grasscutter/game/world/World.java
@@ -39,7 +39,7 @@ public class World implements Iterable {
     @Getter private boolean timeLocked;
 
     private long lastUpdateTime;
-    @Getter private int tickCount = 0;
+    @Getter protected int tickCount = 0;
     @Getter private boolean isPaused = false;
     @Getter private long currentWorldTime;
 
diff --git a/src/main/java/emu/grasscutter/server/packet/recv/HandlerHomeAvatarRewardEventGetReq.java b/src/main/java/emu/grasscutter/server/packet/recv/HandlerHomeAvatarRewardEventGetReq.java
new file mode 100644
index 000000000..6a5482edf
--- /dev/null
+++ b/src/main/java/emu/grasscutter/server/packet/recv/HandlerHomeAvatarRewardEventGetReq.java
@@ -0,0 +1,30 @@
+package emu.grasscutter.server.packet.recv;
+
+import emu.grasscutter.net.packet.Opcodes;
+import emu.grasscutter.net.packet.PacketHandler;
+import emu.grasscutter.net.packet.PacketOpcodes;
+import emu.grasscutter.net.proto.HomeAvatarRewardEventGetReqOuterClass;
+import emu.grasscutter.server.game.GameSession;
+import emu.grasscutter.server.packet.send.PacketHomeAvatarAllFinishRewardNotify;
+import emu.grasscutter.server.packet.send.PacketHomeAvatarRewardEventGetRsp;
+import emu.grasscutter.server.packet.send.PacketHomeAvatarRewardEventNotify;
+
+@Opcodes(PacketOpcodes.HomeAvatarRewardEventGetReq)
+public class HandlerHomeAvatarRewardEventGetReq extends PacketHandler {
+    @Override
+    public void handle(GameSession session, byte[] header, byte[] payload) throws Exception {
+        var req = HomeAvatarRewardEventGetReqOuterClass.HomeAvatarRewardEventGetReq.parseFrom(payload);
+
+        var player = session.getPlayer();
+        var rewardsOrError = player.getCurHomeWorld().getModuleManager().claimAvatarRewards(req.getEventId());
+        session.send(new PacketHomeAvatarRewardEventNotify(player));
+        session.send(new PacketHomeAvatarAllFinishRewardNotify(player));
+
+        session.send(
+            rewardsOrError.map(
+                gameItems -> new PacketHomeAvatarRewardEventGetRsp(req.getEventId(), gameItems),
+                integer -> new PacketHomeAvatarRewardEventGetRsp(req.getEventId(), integer)
+            )
+        );
+    }
+}
diff --git a/src/main/java/emu/grasscutter/server/packet/recv/HandlerHomeAvatarSummonEventReq.java b/src/main/java/emu/grasscutter/server/packet/recv/HandlerHomeAvatarSummonEventReq.java
new file mode 100644
index 000000000..c650e0110
--- /dev/null
+++ b/src/main/java/emu/grasscutter/server/packet/recv/HandlerHomeAvatarSummonEventReq.java
@@ -0,0 +1,19 @@
+package emu.grasscutter.server.packet.recv;
+
+import emu.grasscutter.net.packet.Opcodes;
+import emu.grasscutter.net.packet.PacketHandler;
+import emu.grasscutter.net.packet.PacketOpcodes;
+import emu.grasscutter.net.proto.HomeAvatarSummonEventReqOuterClass;
+import emu.grasscutter.server.game.GameSession;
+import emu.grasscutter.server.packet.send.PacketHomeAvatarSummonEventRsp;
+
+@Opcodes(PacketOpcodes.HomeAvatarSummonEventReq)
+public class HandlerHomeAvatarSummonEventReq extends PacketHandler {
+    @Override
+    public void handle(GameSession session, byte[] header, byte[] payload) throws Exception {
+        var req = HomeAvatarSummonEventReqOuterClass.HomeAvatarSummonEventReq.parseFrom(payload);
+        var moduleManager = session.getPlayer().getCurHomeWorld().getModuleManager();
+        var eventOrError = moduleManager.fireAvatarSummonEvent(session.getPlayer(), req.getAvatarId(), req.getGuid(), req.getSuitId());
+        session.send(eventOrError.map(PacketHomeAvatarSummonEventRsp::new, PacketHomeAvatarSummonEventRsp::new));
+    }
+}
diff --git a/src/main/java/emu/grasscutter/server/packet/recv/HandlerHomeAvatarSummonFinishReq.java b/src/main/java/emu/grasscutter/server/packet/recv/HandlerHomeAvatarSummonFinishReq.java
new file mode 100644
index 000000000..4027c041a
--- /dev/null
+++ b/src/main/java/emu/grasscutter/server/packet/recv/HandlerHomeAvatarSummonFinishReq.java
@@ -0,0 +1,21 @@
+package emu.grasscutter.server.packet.recv;
+
+import emu.grasscutter.net.packet.Opcodes;
+import emu.grasscutter.net.packet.PacketHandler;
+import emu.grasscutter.net.packet.PacketOpcodes;
+import emu.grasscutter.net.proto.HomeAvatarSummonFinishReqOuterClass;
+import emu.grasscutter.server.game.GameSession;
+import emu.grasscutter.server.packet.send.PacketHomeAvatarSummonAllEventNotify;
+import emu.grasscutter.server.packet.send.PacketHomeAvatarSummonFinishRsp;
+
+@Opcodes(PacketOpcodes.HomeAvatarSummonFinishReq)
+public class HandlerHomeAvatarSummonFinishReq extends PacketHandler {
+    @Override
+    public void handle(GameSession session, byte[] header, byte[] payload) throws Exception {
+        var req = HomeAvatarSummonFinishReqOuterClass.HomeAvatarSummonFinishReq.parseFrom(payload);
+        var player = session.getPlayer();
+        player.getCurHomeWorld().getModuleManager().onFinishSummonEvent(req.getEventId());
+        session.send(new PacketHomeAvatarSummonAllEventNotify(session.getPlayer()));
+        session.send(new PacketHomeAvatarSummonFinishRsp(req.getEventId()));
+    }
+}
diff --git a/src/main/java/emu/grasscutter/server/packet/recv/HandlerHomeChangeEditModeReq.java b/src/main/java/emu/grasscutter/server/packet/recv/HandlerHomeChangeEditModeReq.java
index 4d6ae5dfa..3befd5b9e 100644
--- a/src/main/java/emu/grasscutter/server/packet/recv/HandlerHomeChangeEditModeReq.java
+++ b/src/main/java/emu/grasscutter/server/packet/recv/HandlerHomeChangeEditModeReq.java
@@ -1,5 +1,6 @@
 package emu.grasscutter.server.packet.recv;
 
+import emu.grasscutter.game.home.HomeScene;
 import emu.grasscutter.net.packet.Opcodes;
 import emu.grasscutter.net.packet.PacketHandler;
 import emu.grasscutter.net.packet.PacketOpcodes;
@@ -31,14 +32,8 @@ public class HandlerHomeChangeEditModeReq extends PacketHandler {
         session.send(new PacketHomeComfortInfoNotify(session.getPlayer()));
 
         if (!req.getIsEnterEditMode()) {
-            var scene = session.getPlayer().getScene();
-            scene.addEntities(
-                    session
-                            .getPlayer()
-                            .getCurHomeWorld()
-                            .getHome()
-                            .getHomeSceneItem(scene.getId())
-                            .getAnimals(scene));
+            var scene = (HomeScene) session.getPlayer().getScene();
+            scene.onLeaveEditMode();
         }
 
         session.send(new PacketHomeChangeEditModeRsp(req.getIsEnterEditMode()));
diff --git a/src/main/java/emu/grasscutter/server/packet/recv/HandlerHomeChangeModuleReq.java b/src/main/java/emu/grasscutter/server/packet/recv/HandlerHomeChangeModuleReq.java
index cff46aab2..f18e5c141 100644
--- a/src/main/java/emu/grasscutter/server/packet/recv/HandlerHomeChangeModuleReq.java
+++ b/src/main/java/emu/grasscutter/server/packet/recv/HandlerHomeChangeModuleReq.java
@@ -1,7 +1,5 @@
 package emu.grasscutter.server.packet.recv;
 
-import emu.grasscutter.game.world.Position;
-import emu.grasscutter.game.world.Scene;
 import emu.grasscutter.net.packet.Opcodes;
 import emu.grasscutter.net.packet.PacketHandler;
 import emu.grasscutter.net.packet.PacketOpcodes;
@@ -19,9 +17,10 @@ public class HandlerHomeChangeModuleReq extends PacketHandler {
     @Override
     public void handle(GameSession session, byte[] header, byte[] payload) throws Exception {
         HomeChangeModuleReqOuterClass.HomeChangeModuleReq req =
-                HomeChangeModuleReqOuterClass.HomeChangeModuleReq.parseFrom(payload);
+            HomeChangeModuleReqOuterClass.HomeChangeModuleReq.parseFrom(payload);
 
-        if (!session.getPlayer().getCurHomeWorld().getGuests().isEmpty()) {
+        var homeWorld = session.getPlayer().getCurHomeWorld();
+        if (!homeWorld.getGuests().isEmpty()) {
             session.send(new PacketHomeChangeModuleRsp());
             return;
         }
@@ -33,13 +32,10 @@ public class HandlerHomeChangeModuleReq extends PacketHandler {
         session.send(new PacketHomeComfortInfoNotify(session.getPlayer()));
 
         int realmId = 2000 + req.getTargetModuleId();
+        var scene = homeWorld.getSceneById(realmId);
+        var pos = scene.getScriptManager().getConfig().born_pos;
 
-        Scene scene = session.getPlayer().getWorld().getSceneById(realmId);
-        Position pos = scene.getScriptManager().getConfig().born_pos;
-
-        session
-                .getPlayer()
-                .getWorld()
-                .transferPlayerToScene(session.getPlayer(), realmId, TeleportType.WAYPOINT, pos);
+        homeWorld.transferPlayerToScene(session.getPlayer(), realmId, TeleportType.WAYPOINT, pos);
+        homeWorld.refreshModuleManager();
     }
 }
diff --git a/src/main/java/emu/grasscutter/server/packet/recv/HandlerHomeEnterEditModeFinishReq.java b/src/main/java/emu/grasscutter/server/packet/recv/HandlerHomeEnterEditModeFinishReq.java
index 7890726e0..9f4f8f5ae 100644
--- a/src/main/java/emu/grasscutter/server/packet/recv/HandlerHomeEnterEditModeFinishReq.java
+++ b/src/main/java/emu/grasscutter/server/packet/recv/HandlerHomeEnterEditModeFinishReq.java
@@ -1,10 +1,9 @@
 package emu.grasscutter.server.packet.recv;
 
-import emu.grasscutter.game.entity.EntityHomeAnimal;
+import emu.grasscutter.game.home.HomeScene;
 import emu.grasscutter.net.packet.Opcodes;
 import emu.grasscutter.net.packet.PacketHandler;
 import emu.grasscutter.net.packet.PacketOpcodes;
-import emu.grasscutter.net.proto.VisionTypeOuterClass;
 import emu.grasscutter.server.game.GameSession;
 import emu.grasscutter.server.packet.send.PacketHomeEnterEditModeFinishRsp;
 
@@ -17,12 +16,8 @@ public class HandlerHomeEnterEditModeFinishReq extends PacketHandler {
          * This packet is about the edit mode
          */
 
-        var scene = session.getPlayer().getScene();
-        scene.removeEntities(
-                scene.getEntities().values().stream()
-                        .filter(gameEntity -> gameEntity instanceof EntityHomeAnimal)
-                        .toList(),
-                VisionTypeOuterClass.VisionType.VISION_TYPE_REMOVE);
+        var scene = (HomeScene) session.getPlayer().getScene();
+        scene.onEnterEditModeFinish();
 
         session.send(new PacketHomeEnterEditModeFinishRsp());
     }
diff --git a/src/main/java/emu/grasscutter/server/packet/recv/HandlerHomeSceneInitFinishReq.java b/src/main/java/emu/grasscutter/server/packet/recv/HandlerHomeSceneInitFinishReq.java
index 2932a663b..a86e8f32c 100644
--- a/src/main/java/emu/grasscutter/server/packet/recv/HandlerHomeSceneInitFinishReq.java
+++ b/src/main/java/emu/grasscutter/server/packet/recv/HandlerHomeSceneInitFinishReq.java
@@ -1,6 +1,8 @@
 package emu.grasscutter.server.packet.recv;
 
-import emu.grasscutter.net.packet.*;
+import emu.grasscutter.net.packet.Opcodes;
+import emu.grasscutter.net.packet.PacketHandler;
+import emu.grasscutter.net.packet.PacketOpcodes;
 import emu.grasscutter.net.proto.OtherPlayerEnterHomeNotifyOuterClass;
 import emu.grasscutter.server.game.GameSession;
 import emu.grasscutter.server.packet.send.*;
@@ -16,16 +18,20 @@ public class HandlerHomeSceneInitFinishReq extends PacketHandler {
             session.getPlayer().setHasSentInitPacketInHome(true);
 
             if (curHomeWorld.getHost().isOnline()
-                    && !curHomeWorld.getHost().equals(session.getPlayer())) {
+                && !curHomeWorld.getHost().equals(session.getPlayer())) {
                 curHomeWorld
-                        .getHost()
-                        .sendPacket(
-                                new PacketOtherPlayerEnterOrLeaveHomeNotify(
-                                        session.getPlayer(),
-                                        OtherPlayerEnterHomeNotifyOuterClass.OtherPlayerEnterHomeNotify.Reason.ENTER));
+                    .getHost()
+                    .sendPacket(
+                        new PacketOtherPlayerEnterOrLeaveHomeNotify(
+                            session.getPlayer(),
+                            OtherPlayerEnterHomeNotifyOuterClass.OtherPlayerEnterHomeNotify.Reason.ENTER));
             }
         }
 
+        curHomeWorld.ifHost(session.getPlayer(), player -> {
+            player.sendPacket(new PacketHomeAvatarRewardEventNotify(player));
+            player.sendPacket(new PacketHomeAvatarSummonAllEventNotify(player));
+        });
         session.send(new PacketHomeMarkPointNotify(session.getPlayer()));
 
         session.send(new PacketHomeSceneInitFinishRsp());
diff --git a/src/main/java/emu/grasscutter/server/packet/recv/HandlerHomeUpdateArrangementInfoReq.java b/src/main/java/emu/grasscutter/server/packet/recv/HandlerHomeUpdateArrangementInfoReq.java
index 2b93ac718..e605dc091 100644
--- a/src/main/java/emu/grasscutter/server/packet/recv/HandlerHomeUpdateArrangementInfoReq.java
+++ b/src/main/java/emu/grasscutter/server/packet/recv/HandlerHomeUpdateArrangementInfoReq.java
@@ -5,10 +5,7 @@ import emu.grasscutter.net.packet.PacketHandler;
 import emu.grasscutter.net.packet.PacketOpcodes;
 import emu.grasscutter.net.proto.HomeUpdateArrangementInfoReqOuterClass;
 import emu.grasscutter.server.game.GameSession;
-import emu.grasscutter.server.packet.send.PacketHomeAvatarTalkFinishInfoNotify;
-import emu.grasscutter.server.packet.send.PacketHomeBasicInfoNotify;
-import emu.grasscutter.server.packet.send.PacketHomeMarkPointNotify;
-import emu.grasscutter.server.packet.send.PacketHomeUpdateArrangementInfoRsp;
+import emu.grasscutter.server.packet.send.*;
 
 @Opcodes(PacketOpcodes.HomeUpdateArrangementInfoReq)
 public class HandlerHomeUpdateArrangementInfoReq extends PacketHandler {
@@ -22,14 +19,17 @@ public class HandlerHomeUpdateArrangementInfoReq extends PacketHandler {
                 session.getPlayer().getHome().getHomeSceneItem(session.getPlayer().getSceneId());
 
         var roomSceneId = homeScene.getRoomSceneId();
-        homeScene.update(req.getSceneArrangementInfo());
+        homeScene.update(req.getSceneArrangementInfo(), session.getPlayer());
         if (roomSceneId != homeScene.getRoomSceneId()) {
             session.getPlayer().getHome().onMainHouseChanged();
         }
 
+        session.getPlayer().getCurHomeWorld().getModuleManager().onUpdateArrangement();
+        session.send(new PacketHomeAvatarRewardEventNotify(session.getPlayer()));
         session.send(
                 new PacketHomeBasicInfoNotify(session.getPlayer(), session.getPlayer().isInEditMode()));
         session.send(new PacketHomeAvatarTalkFinishInfoNotify(session.getPlayer()));
+        session.send(new PacketHomeAvatarSummonAllEventNotify(session.getPlayer()));
         session.send(new PacketHomeMarkPointNotify(session.getPlayer()));
 
         session.getPlayer().getHome().save();
diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketHomeAvatarAllFinishRewardNotify.java b/src/main/java/emu/grasscutter/server/packet/send/PacketHomeAvatarAllFinishRewardNotify.java
new file mode 100644
index 000000000..eda4ae116
--- /dev/null
+++ b/src/main/java/emu/grasscutter/server/packet/send/PacketHomeAvatarAllFinishRewardNotify.java
@@ -0,0 +1,18 @@
+package emu.grasscutter.server.packet.send;
+
+import emu.grasscutter.game.player.Player;
+import emu.grasscutter.net.packet.BasePacket;
+import emu.grasscutter.net.packet.PacketOpcodes;
+import emu.grasscutter.net.proto.HomeAvatarAllFinishRewardNotifyOuterClass;
+
+public class PacketHomeAvatarAllFinishRewardNotify extends BasePacket {
+    public PacketHomeAvatarAllFinishRewardNotify(Player player) {
+        super(PacketOpcodes.HomeAvatarAllFinishRewardNotify);
+
+        var list = player.getHome().getFinishedRewardEventIdSet();
+        if (list != null) {
+            this.setData(HomeAvatarAllFinishRewardNotifyOuterClass.HomeAvatarAllFinishRewardNotify.newBuilder()
+                .addAllEventIdList(player.getHome().getFinishedRewardEventIdSet()));
+        }
+    }
+}
diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketHomeAvatarRewardEventGetRsp.java b/src/main/java/emu/grasscutter/server/packet/send/PacketHomeAvatarRewardEventGetRsp.java
new file mode 100644
index 000000000..73aa4d762
--- /dev/null
+++ b/src/main/java/emu/grasscutter/server/packet/send/PacketHomeAvatarRewardEventGetRsp.java
@@ -0,0 +1,26 @@
+package emu.grasscutter.server.packet.send;
+
+import emu.grasscutter.game.inventory.GameItem;
+import emu.grasscutter.net.packet.BasePacket;
+import emu.grasscutter.net.packet.PacketOpcodes;
+import emu.grasscutter.net.proto.HomeAvatarRewardEventGetRspOuterClass;
+
+import java.util.List;
+
+public class PacketHomeAvatarRewardEventGetRsp extends BasePacket {
+    public PacketHomeAvatarRewardEventGetRsp(int eventId, List rewards) {
+        super(PacketOpcodes.HomeAvatarRewardEventGetRsp);
+
+        this.setData(HomeAvatarRewardEventGetRspOuterClass.HomeAvatarRewardEventGetRsp.newBuilder()
+            .setEventId(eventId)
+            .addAllItemList(rewards.stream().map(GameItem::toItemParam).toList()));
+    }
+
+    public PacketHomeAvatarRewardEventGetRsp(int eventId, int retcode) {
+        super(PacketOpcodes.HomeAvatarRewardEventGetRsp);
+
+        this.setData(HomeAvatarRewardEventGetRspOuterClass.HomeAvatarRewardEventGetRsp.newBuilder()
+            .setEventId(eventId)
+            .setRetcode(retcode));
+    }
+}
diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketHomeAvatarRewardEventNotify.java b/src/main/java/emu/grasscutter/server/packet/send/PacketHomeAvatarRewardEventNotify.java
new file mode 100644
index 000000000..cea81c2ae
--- /dev/null
+++ b/src/main/java/emu/grasscutter/server/packet/send/PacketHomeAvatarRewardEventNotify.java
@@ -0,0 +1,12 @@
+package emu.grasscutter.server.packet.send;
+
+import emu.grasscutter.game.player.Player;
+import emu.grasscutter.net.packet.BasePacket;
+import emu.grasscutter.net.packet.PacketOpcodes;
+
+public class PacketHomeAvatarRewardEventNotify extends BasePacket {
+    public PacketHomeAvatarRewardEventNotify(Player homeOwner) {
+        super(PacketOpcodes.HomeAvatarRewardEventNotify);
+        this.setData(homeOwner.getCurHomeWorld().getModuleManager().toRewardEventProto());
+    }
+}
diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketHomeAvatarSummonAllEventNotify.java b/src/main/java/emu/grasscutter/server/packet/send/PacketHomeAvatarSummonAllEventNotify.java
new file mode 100644
index 000000000..70925d38c
--- /dev/null
+++ b/src/main/java/emu/grasscutter/server/packet/send/PacketHomeAvatarSummonAllEventNotify.java
@@ -0,0 +1,12 @@
+package emu.grasscutter.server.packet.send;
+
+import emu.grasscutter.game.player.Player;
+import emu.grasscutter.net.packet.BasePacket;
+import emu.grasscutter.net.packet.PacketOpcodes;
+
+public class PacketHomeAvatarSummonAllEventNotify extends BasePacket {
+    public PacketHomeAvatarSummonAllEventNotify(Player homeOwner) {
+        super(PacketOpcodes.HomeAvatarSummonAllEventNotify);
+        this.setData(homeOwner.getCurHomeWorld().getModuleManager().toSummonEventProto());
+    }
+}
diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketHomeAvatarSummonEventRsp.java b/src/main/java/emu/grasscutter/server/packet/send/PacketHomeAvatarSummonEventRsp.java
new file mode 100644
index 000000000..6c28fb695
--- /dev/null
+++ b/src/main/java/emu/grasscutter/server/packet/send/PacketHomeAvatarSummonEventRsp.java
@@ -0,0 +1,22 @@
+package emu.grasscutter.server.packet.send;
+
+import emu.grasscutter.game.home.suite.event.HomeAvatarSummonEvent;
+import emu.grasscutter.net.packet.BasePacket;
+import emu.grasscutter.net.packet.PacketOpcodes;
+import emu.grasscutter.net.proto.HomeAvatarSummonEventRspOuterClass;
+
+public class PacketHomeAvatarSummonEventRsp extends BasePacket {
+    public PacketHomeAvatarSummonEventRsp(HomeAvatarSummonEvent event) {
+        super(PacketOpcodes.HomeAvatarSummonEventRsp);
+
+        this.setData(HomeAvatarSummonEventRspOuterClass.HomeAvatarSummonEventRsp.newBuilder()
+            .setEventId(event.getEventId()));
+    }
+
+    public PacketHomeAvatarSummonEventRsp(int retcode) {
+        super(PacketOpcodes.HomeAvatarSummonEventRsp);
+
+        this.setData(HomeAvatarSummonEventRspOuterClass.HomeAvatarSummonEventRsp.newBuilder()
+            .setRetcode(retcode));
+    }
+}
diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketHomeAvatarSummonFinishRsp.java b/src/main/java/emu/grasscutter/server/packet/send/PacketHomeAvatarSummonFinishRsp.java
new file mode 100644
index 000000000..66ad749e4
--- /dev/null
+++ b/src/main/java/emu/grasscutter/server/packet/send/PacketHomeAvatarSummonFinishRsp.java
@@ -0,0 +1,14 @@
+package emu.grasscutter.server.packet.send;
+
+import emu.grasscutter.net.packet.BasePacket;
+import emu.grasscutter.net.packet.PacketOpcodes;
+import emu.grasscutter.net.proto.HomeAvatarSummonFinishRspOuterClass;
+
+public class PacketHomeAvatarSummonFinishRsp extends BasePacket {
+    public PacketHomeAvatarSummonFinishRsp(int eventId) {
+        super(PacketOpcodes.HomeAvatarSummonFinishRsp);
+
+        this.setData(HomeAvatarSummonFinishRspOuterClass.HomeAvatarSummonFinishRsp.newBuilder()
+            .setEventId(eventId));
+    }
+}
diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketPlayerQuitFromHomeNotify.java b/src/main/java/emu/grasscutter/server/packet/send/PacketPlayerQuitFromHomeNotify.java
new file mode 100644
index 000000000..2c5a82332
--- /dev/null
+++ b/src/main/java/emu/grasscutter/server/packet/send/PacketPlayerQuitFromHomeNotify.java
@@ -0,0 +1,14 @@
+package emu.grasscutter.server.packet.send;
+
+import emu.grasscutter.net.packet.BasePacket;
+import emu.grasscutter.net.packet.PacketOpcodes;
+import emu.grasscutter.net.proto.PlayerQuitFromHomeNotifyOuterClass;
+
+public class PacketPlayerQuitFromHomeNotify extends BasePacket {
+    public PacketPlayerQuitFromHomeNotify(PlayerQuitFromHomeNotifyOuterClass.PlayerQuitFromHomeNotify.QuitReason reason) {
+        super(PacketOpcodes.PlayerQuitFromHomeNotify);
+
+        this.setData(PlayerQuitFromHomeNotifyOuterClass.PlayerQuitFromHomeNotify.newBuilder()
+            .setReason(reason));
+    }
+}
diff --git a/src/main/java/emu/grasscutter/utils/Either.java b/src/main/java/emu/grasscutter/utils/Either.java
new file mode 100644
index 000000000..b976fdfbd
--- /dev/null
+++ b/src/main/java/emu/grasscutter/utils/Either.java
@@ -0,0 +1,153 @@
+package emu.grasscutter.utils;
+
+import java.util.Objects;
+import java.util.Optional;
+import java.util.function.Consumer;
+import java.util.function.Function;
+
+public abstract class Either {
+    private static final class Left extends Either {
+        private final L value;
+
+        public Left(L value) {
+            this.value = value;
+        }
+
+        @Override
+        public  Either mapBoth(Function f1, Function f2) {
+            return new Left<>(f1.apply(this.value));
+        }
+
+        @Override
+        public  T map(Function l, Function r) {
+            return l.apply(this.value);
+        }
+
+        @Override
+        public Either ifLeft(Consumer consumer) {
+            consumer.accept(this.value);
+            return this;
+        }
+
+        @Override
+        public Either ifRight(Consumer consumer) {
+            return this;
+        }
+
+        @Override
+        public Optional left() {
+            return Optional.of(this.value);
+        }
+
+        @Override
+        public Optional right() {
+            return Optional.empty();
+        }
+
+        @Override
+        public String toString() {
+            return "Left[" + this.value + "]";
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (this == o) return true;
+            if (o == null || getClass() != o.getClass()) return false;
+            Left left = (Left) o;
+            return Objects.equals(value, left.value);
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(value);
+        }
+    }
+
+    private static final class Right extends Either {
+        private final R value;
+
+        public Right(R value) {
+            this.value = value;
+        }
+
+        @Override
+        public  Either mapBoth(Function f1, Function f2) {
+            return new Right<>(f2.apply(this.value));
+        }
+
+        @Override
+        public  T map(Function l, Function r) {
+            return r.apply(this.value);
+        }
+
+        @Override
+        public Either ifLeft(Consumer consumer) {
+            return this;
+        }
+
+        @Override
+        public Either ifRight(Consumer consumer) {
+            consumer.accept(this.value);
+            return this;
+        }
+
+        @Override
+        public Optional left() {
+            return Optional.empty();
+        }
+
+        @Override
+        public Optional right() {
+            return Optional.of(this.value);
+        }
+
+        @Override
+        public String toString() {
+            return "Right[" + this.value + "]";
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (this == o) return true;
+            if (o == null || getClass() != o.getClass()) return false;
+            Right right = (Right) o;
+            return Objects.equals(value, right.value);
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(value);
+        }
+    }
+
+    private Either() {
+    }
+
+    public abstract  Either mapBoth(Function f1, Function f2);
+
+    public abstract  T map(Function l, Function r);
+
+    public abstract Either ifLeft(Consumer consumer);
+
+    public abstract Either ifRight(Consumer consumer);
+
+    public abstract Optional left();
+
+    public abstract Optional right();
+
+    public  Either mapLeft(Function l) {
+        return map(t -> left(l.apply(t)), Either::right);
+    }
+
+    public  Either mapRight(Function l) {
+        return map(Either::left, t -> right(l.apply(t)));
+    }
+
+    public static  Either left(L value) {
+        return new Left<>(value);
+    }
+
+    public static  Either right(R value) {
+        return new Right<>(value);
+    }
+}