From 68088e00e926d22ac747c453289f21e56f380894 Mon Sep 17 00:00:00 2001 From: edwinQQQ Date: Tue, 9 Sep 2025 15:54:36 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E6=88=BF=E9=97=B4=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=EF=BC=8C=E6=96=B0=E5=A2=9E=20MicCpInfoModel=20?= =?UTF-8?q?=E7=B1=BB=E4=BB=A5=E7=AE=A1=E7=90=86=E9=BA=A6=E4=BD=8D=E5=85=B3?= =?UTF-8?q?=E7=B3=BB=E6=95=B0=E6=8D=AE=EF=BC=8C=E4=BC=98=E5=8C=96=E6=88=BF?= =?UTF-8?q?=E9=97=B4=E4=B8=AD=E7=82=B9=E7=9F=A9=E5=BD=A2=E5=92=8C=E9=BA=A6?= =?UTF-8?q?=E4=BD=8D=E7=8A=B6=E6=80=81=E7=AE=A1=E7=90=86=E9=80=BB=E8=BE=91?= =?UTF-8?q?=E3=80=82=E5=90=8C=E6=97=B6=EF=BC=8C=E6=9B=B4=E6=96=B0=E7=9B=B8?= =?UTF-8?q?=E5=85=B3=20Presenter=20=E5=92=8C=20ViewController=20=E4=BB=A5?= =?UTF-8?q?=E9=9B=86=E6=88=90=E6=96=B0=E7=9A=84=E9=BA=A6=E4=BD=8D=E5=85=B3?= =?UTF-8?q?=E7=B3=BB=20API=EF=BC=8C=E6=8F=90=E5=8D=87=E4=BB=A3=E7=A0=81?= =?UTF-8?q?=E5=8F=AF=E7=BB=B4=E6=8A=A4=E6=80=A7=E5=92=8C=E7=94=A8=E6=88=B7?= =?UTF-8?q?=E4=BD=93=E9=AA=8C=E3=80=82=E6=96=B0=E5=A2=9E=E9=BA=A6=E4=BD=8D?= =?UTF-8?q?=E7=8A=B6=E6=80=81=E7=9B=91=E6=B5=8B=E5=92=8C=E6=9B=B4=E6=96=B0?= =?UTF-8?q?=E6=9C=BA=E5=88=B6=EF=BC=8C=E7=A1=AE=E4=BF=9D=E7=94=A8=E6=88=B7?= =?UTF-8?q?=E5=9C=A8=E6=88=BF=E9=97=B4=E4=B8=AD=E7=9A=84=E9=BA=A6=E4=BD=8D?= =?UTF-8?q?=E4=BF=A1=E6=81=AF=E5=AE=9E=E6=97=B6=E5=87=86=E7=A1=AE=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- YuMi.xcodeproj/project.pbxproj | 6 + YuMi/Appdelegate/AppDelegate+ThirdConfig.m | 12 - .../Modules/YMMessage/Model/AttachmentModel.h | 12 +- .../YMMine/View/XPMineViewController.m | 3 + YuMi/Modules/YMRoom/Api/Api+Room.h | 3 + YuMi/Modules/YMRoom/Api/Api+Room.m | 9 + .../Features/Boom/BoomInfoViewController.m | 41 ++ YuMi/Modules/YMRoom/Model/MicCpInfoModel.h | 20 + YuMi/Modules/YMRoom/Model/MicCpInfoModel.m | 12 + .../YMRoom/Presenter/XPRoomPresenter.h | 4 + .../YMRoom/Presenter/XPRoomPresenter.m | 26 + YuMi/Modules/YMRoom/Protocol/XPRoomProtocol.h | 8 +- .../View/FaceView/Presenter/XPRoomFaceTool.m | 4 - .../View/StageView/MicMidpointRectManager.h | 56 +- .../View/StageView/MicMidpointRectManager.m | 270 ++++++- .../View/StageView/MicroView/MicroView.m | 31 +- .../YMRoom/View/XPRoomViewController.m | 681 ++++++++++++++++-- 17 files changed, 1053 insertions(+), 145 deletions(-) create mode 100644 YuMi/Modules/YMRoom/Model/MicCpInfoModel.h create mode 100644 YuMi/Modules/YMRoom/Model/MicCpInfoModel.m diff --git a/YuMi.xcodeproj/project.pbxproj b/YuMi.xcodeproj/project.pbxproj index c934bbb0..c0e32a8c 100644 --- a/YuMi.xcodeproj/project.pbxproj +++ b/YuMi.xcodeproj/project.pbxproj @@ -554,6 +554,7 @@ 4C886BEB2E014AE5006F0BA7 /* MedalsPresenter.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C886BEA2E014AE5006F0BA7 /* MedalsPresenter.m */; }; 4C886BEE2E014B6C006F0BA7 /* Api+Medals.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C886BED2E014B6C006F0BA7 /* Api+Medals.m */; }; 4C886BF22E015D61006F0BA7 /* MedalsModel.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C886BF12E015D61006F0BA7 /* MedalsModel.m */; }; + 4C9828132E6EB50000FC6345 /* MicCpInfoModel.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C9828122E6EB50000FC6345 /* MicCpInfoModel.m */; }; 4CA532B42D5AEE9400B8F59F /* Api+LuckyPackage.m in Sources */ = {isa = PBXBuildFile; fileRef = 4CA532B32D5AEE9400B8F59F /* Api+LuckyPackage.m */; }; 4CA532B72D5B333200B8F59F /* RoomLuckyPackageInfoModel.m in Sources */ = {isa = PBXBuildFile; fileRef = 4CA532B62D5B333200B8F59F /* RoomLuckyPackageInfoModel.m */; }; 4CA532BA2D5C8EBE00B8F59F /* LuckyPackageBannerView.m in Sources */ = {isa = PBXBuildFile; fileRef = 4CA532B92D5C8EBE00B8F59F /* LuckyPackageBannerView.m */; }; @@ -2783,6 +2784,8 @@ 4C886BED2E014B6C006F0BA7 /* Api+Medals.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "Api+Medals.m"; sourceTree = ""; }; 4C886BF02E015D61006F0BA7 /* MedalsModel.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MedalsModel.h; sourceTree = ""; }; 4C886BF12E015D61006F0BA7 /* MedalsModel.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MedalsModel.m; sourceTree = ""; }; + 4C9828112E6EB50000FC6345 /* MicCpInfoModel.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MicCpInfoModel.h; sourceTree = ""; }; + 4C9828122E6EB50000FC6345 /* MicCpInfoModel.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MicCpInfoModel.m; sourceTree = ""; }; 4CA532B22D5AEE9400B8F59F /* Api+LuckyPackage.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Api+LuckyPackage.h"; sourceTree = ""; }; 4CA532B32D5AEE9400B8F59F /* Api+LuckyPackage.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "Api+LuckyPackage.m"; sourceTree = ""; }; 4CA532B52D5B333200B8F59F /* RoomLuckyPackageInfoModel.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RoomLuckyPackageInfoModel.h; sourceTree = ""; }; @@ -7923,6 +7926,8 @@ 4C75CEFA2D6318FF009147A5 /* RoomEnterModel.m */, 4CFBE0C82DAD085700A923AF /* BravoGiftTabInfomationModel.h */, 4CFBE0C92DAD085700A923AF /* BravoGiftTabInfomationModel.m */, + 4C9828112E6EB50000FC6345 /* MicCpInfoModel.h */, + 4C9828122E6EB50000FC6345 /* MicCpInfoModel.m */, ); path = Model; sourceTree = ""; @@ -12738,6 +12743,7 @@ E82325F2274E2DE6003A3332 /* XPUserCardViewController.m in Sources */, 4CA532B72D5B333200B8F59F /* RoomLuckyPackageInfoModel.m in Sources */, E85E7B512A4EB0D300B6D00A /* Api+Guild.m in Sources */, + 4C9828132E6EB50000FC6345 /* MicCpInfoModel.m in Sources */, E83645682A40A2DC00E0DBE4 /* XPSkillCardPlayerManager.m in Sources */, E8F65C222869A36F009BB5B9 /* ContentShareMonentsModel.m in Sources */, 9B6E856E281AABAB0041A321 /* XPRoomRecommendModel.m in Sources */, diff --git a/YuMi/Appdelegate/AppDelegate+ThirdConfig.m b/YuMi/Appdelegate/AppDelegate+ThirdConfig.m index 12fb5ee5..2dff5f9a 100644 --- a/YuMi/Appdelegate/AppDelegate+ThirdConfig.m +++ b/YuMi/Appdelegate/AppDelegate+ThirdConfig.m @@ -160,9 +160,6 @@ UIKIT_EXTERN NSString * adImageName; info.identifier = emotionDic[@"id"]; info.image = image; - // 添加调试日志 - NSLog(@"加载表情: %@, 图片: %@, 是否成功: %@", info.displayName, emotionDic[@"file"], image ? @"是" : @"否"); - [array addObject:info]; } //在这里强烈建议先预加载一下表情 @@ -171,16 +168,7 @@ UIKIT_EXTERN NSString * adImageName; // 清理 emoji 缓存,确保新的尺寸设置生效 [QEmotionHelper clearEmojiCache]; - -#if DEBUG - // 测试图片加载 - NSLog(@"表情数组加载完成,总数: %lu", (unsigned long)array.count); - for (int i = 0; i < MIN(array.count, 3); i++) { - QEmotion *emotion = array[i]; - NSLog(@"测试表情 %d: %@, 图片: %@", i, emotion.displayName, emotion.image ? @"加载成功" : @"加载失败"); - } }); -#endif } #pragma mark - 广告 diff --git a/YuMi/Modules/YMMessage/Model/AttachmentModel.h b/YuMi/Modules/YMMessage/Model/AttachmentModel.h index 2d3fec40..d2f3daf6 100644 --- a/YuMi/Modules/YMMessage/Model/AttachmentModel.h +++ b/YuMi/Modules/YMMessage/Model/AttachmentModel.h @@ -156,13 +156,19 @@ typedef NS_ENUM(NSUInteger, CustomMessageType) { /// 客户端独立收发的消息, 从 1000 开始 ClientMessage_Type = 1000, + + MicRelationship_Type = 1001, }; typedef NS_ENUM(NSUInteger, ClientMessageType) { - ClientMessage_UpMic_Ask = 1001, - ClientMessage_UpMic_Agree = 1002, - ClientMessage_UpMic_Reject = 1003, + ClientMessage_UpMic_Ask = 10001, + ClientMessage_UpMic_Agree = 10002, + ClientMessage_UpMic_Reject = 10003, +}; + +typedef NS_ENUM(NSUInteger, MicRelationshipType) { + MicRelationship_CP = 10011, }; diff --git a/YuMi/Modules/YMMine/View/XPMineViewController.m b/YuMi/Modules/YMMine/View/XPMineViewController.m index 05004ddf..57045873 100644 --- a/YuMi/Modules/YMMine/View/XPMineViewController.m +++ b/YuMi/Modules/YMMine/View/XPMineViewController.m @@ -163,6 +163,9 @@ UIKIT_EXTERN NSString *kRequestTicket; // self.isHavePermission = YES; // return; //#endif + + // TODO: 切换账号可能会保持权限状态,需要进一步检查逻辑 + ClientConfig *config = [ClientConfig shareConfig]; NSArray *uidList = config.configInfo.giveDiamondErbanNoList; for (id uid in uidList) { diff --git a/YuMi/Modules/YMRoom/Api/Api+Room.h b/YuMi/Modules/YMRoom/Api/Api+Room.h index 98a801d7..97e5e193 100644 --- a/YuMi/Modules/YMRoom/Api/Api+Room.h +++ b/YuMi/Modules/YMRoom/Api/Api+Room.h @@ -232,6 +232,9 @@ NS_ASSUME_NONNULL_BEGIN + (void)shareGen:(HttpRequestHelperCompletion)completion targetUid:(NSString *)targetUid; ++ (void)getRoomMicCpListByRoomUid:(HttpRequestHelperCompletion)completion roomUid:(NSString *)roomUid; + ++ (void)getRoomMicCpListByUidList:(HttpRequestHelperCompletion)completion uidList:(NSString *)uidList; @end NS_ASSUME_NONNULL_END diff --git a/YuMi/Modules/YMRoom/Api/Api+Room.m b/YuMi/Modules/YMRoom/Api/Api+Room.m index c68dd2af..d8393156 100644 --- a/YuMi/Modules/YMRoom/Api/Api+Room.m +++ b/YuMi/Modules/YMRoom/Api/Api+Room.m @@ -322,4 +322,13 @@ + (void)shareGen:(HttpRequestHelperCompletion)completion targetUid:(NSString *)targetUid { [self makeRequest:@"share/gen" method:HttpRequestHelperMethodPOST completion:completion, __FUNCTION__, targetUid, nil]; } + ++ (void)getRoomMicCpListByRoomUid:(HttpRequestHelperCompletion)completion roomUid:(NSString *)roomUid { + [self makeRequest:@"room/mic/cp/listByRoomUid" method:HttpRequestHelperMethodGET completion:completion, __FUNCTION__, roomUid, nil]; +} + ++ (void)getRoomMicCpListByUidList:(HttpRequestHelperCompletion)completion uidList:(NSArray *)uidList { + [self makeRequest:@"room/mic/cp/listByUidList" method:HttpRequestHelperMethodGET completion:completion, __FUNCTION__, uidList, nil]; +} + @end diff --git a/YuMi/Modules/YMRoom/Features/Boom/BoomInfoViewController.m b/YuMi/Modules/YMRoom/Features/Boom/BoomInfoViewController.m index 1a6bdc6d..0498260e 100644 --- a/YuMi/Modules/YMRoom/Features/Boom/BoomInfoViewController.m +++ b/YuMi/Modules/YMRoom/Features/Boom/BoomInfoViewController.m @@ -133,6 +133,7 @@ @property (nonatomic, strong) UILabel *rankName_3; @property (nonatomic, strong) UILabel *rankTipsLabel; +@property (nonatomic, strong) UILabel *ratioLabel; @end @@ -438,6 +439,12 @@ make.center.mas_equalTo(progressNumberBG); }]; + [progressBG addSubview:self.ratioLabel]; + [self.ratioLabel mas_makeConstraints:^(MASConstraintMaker *make) { + make.centerX.mas_equalTo(progressBG).offset(2); + make.centerY.mas_equalTo(progressBG); + }]; + UIImageView *rocketsBG = [self rocketsBG]; [self.view addSubview:rocketsBG]; [rocketsBG mas_makeConstraints:^(MASConstraintMaker *make) { @@ -681,6 +688,18 @@ progress = 0.1; } + // 更新比值 label 并保持发光效果 + NSString *ratioText = [NSString stringWithFormat:@"%@/%@", + @(boom.exp * boom.speed / 100.0).stringValue, + @(boom.exp).stringValue]; + + NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] initWithString:ratioText]; + [attributedString addAttribute:NSStrokeColorAttributeName value:[UIColor whiteColor] range:NSMakeRange(0, attributedString.length)]; + [attributedString addAttribute:NSStrokeWidthAttributeName value:@(-2.0) range:NSMakeRange(0, attributedString.length)]; + [attributedString addAttribute:NSForegroundColorAttributeName value:[UIColor blackColor] range:NSMakeRange(0, attributedString.length)]; + [attributedString addAttribute:NSFontAttributeName value:kFontMedium(14) range:NSMakeRange(0, attributedString.length)]; + self.ratioLabel.attributedText = attributedString; + [UIView animateWithDuration:0.2 animations:^{ [self.progressBar mas_updateConstraints:^(MASConstraintMaker *make) { make.height.mas_equalTo(kGetScaleWidth(180) * progress); // 动态调整高度 @@ -1120,5 +1139,27 @@ return _rankTipsLabel; } +- (UILabel *)ratioLabel { + if (!_ratioLabel) { + _ratioLabel = [UILabel labelInitWithText:@"0/0" font:kFontMedium(14) textColor:[UIColor blackColor]]; + _ratioLabel.textAlignment = NSTextAlignmentCenter; + _ratioLabel.transform = CGAffineTransformMakeRotation(M_PI_2); // 顺时针旋转90度 + + // 添加发光效果 + _ratioLabel.layer.shadowColor = [UIColor whiteColor].CGColor; + _ratioLabel.layer.shadowOffset = CGSizeZero; + _ratioLabel.layer.shadowRadius = 3.0; + _ratioLabel.layer.shadowOpacity = 0.8; + _ratioLabel.layer.masksToBounds = NO; + + // 添加文字描边效果 + NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] initWithString:@"0/0"]; + [attributedString addAttribute:NSStrokeColorAttributeName value:[UIColor whiteColor] range:NSMakeRange(0, attributedString.length)]; + [attributedString addAttribute:NSStrokeWidthAttributeName value:@(-2.0) range:NSMakeRange(0, attributedString.length)]; + _ratioLabel.attributedText = attributedString; + } + return _ratioLabel; +} + @end diff --git a/YuMi/Modules/YMRoom/Model/MicCpInfoModel.h b/YuMi/Modules/YMRoom/Model/MicCpInfoModel.h new file mode 100644 index 00000000..2a6f06ed --- /dev/null +++ b/YuMi/Modules/YMRoom/Model/MicCpInfoModel.h @@ -0,0 +1,20 @@ +// +// MicCpInfoModel.h +// YuMi +// +// Created by P on 2025/9/8. +// + +#import "PIBaseModel.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface MicCpInfoModel : PIBaseModel + +@property (nonatomic, assign) NSInteger uid; +@property (nonatomic, assign) NSInteger cpLevel; +@property (nonatomic, assign) NSInteger loverUid; + +@end + +NS_ASSUME_NONNULL_END diff --git a/YuMi/Modules/YMRoom/Model/MicCpInfoModel.m b/YuMi/Modules/YMRoom/Model/MicCpInfoModel.m new file mode 100644 index 00000000..c262d75a --- /dev/null +++ b/YuMi/Modules/YMRoom/Model/MicCpInfoModel.m @@ -0,0 +1,12 @@ +// +// MicCpInfoModel.m +// YuMi +// +// Created by P on 2025/9/8. +// + +#import "MicCpInfoModel.h" + +@implementation MicCpInfoModel + +@end diff --git a/YuMi/Modules/YMRoom/Presenter/XPRoomPresenter.h b/YuMi/Modules/YMRoom/Presenter/XPRoomPresenter.h index b68663df..a2511d91 100644 --- a/YuMi/Modules/YMRoom/Presenter/XPRoomPresenter.h +++ b/YuMi/Modules/YMRoom/Presenter/XPRoomPresenter.h @@ -82,6 +82,10 @@ NS_ASSUME_NONNULL_BEGIN - (void)getShareLink:(NSString *)roomUid success:(void(^)(NSString *link))success failure:(void(^)(NSError *error))failure; +- (void)micCpListByRoomUid:(NSString *)roomUid; + +- (void)micCpListByUidList:(NSArray *)uidList; + @end NS_ASSUME_NONNULL_END diff --git a/YuMi/Modules/YMRoom/Presenter/XPRoomPresenter.m b/YuMi/Modules/YMRoom/Presenter/XPRoomPresenter.m index 718639f5..efee49b1 100644 --- a/YuMi/Modules/YMRoom/Presenter/XPRoomPresenter.m +++ b/YuMi/Modules/YMRoom/Presenter/XPRoomPresenter.m @@ -25,6 +25,7 @@ #import "XPRedPacketModel.h" #import "XPFreeGiftModel.h" #import "BoomInfoModel.h" +#import "MicCpInfoModel.h" ///P #import "XPRoomProtocol.h" @@ -343,4 +344,29 @@ } showLoading:YES errorToast:YES] targetUid:roomUid]; } +- (void)micCpListByRoomUid:(NSString *)roomUid { + @kWeakify(self); + [Api getRoomMicCpListByRoomUid:^(BaseModel * _Nullable data, NSInteger code, NSString * _Nullable msg) { + @kStrongify(self); + if (code == 200) { + if ([[self getView] respondsToSelector:@selector(getMicCpListByRoomUidSuccess:)]) { + NSArray *cpList = [MicCpInfoModel modelsWithArray:data.data]; + [[self getView] getMicCpListByRoomUidSuccess:cpList]; + } + } + } roomUid:roomUid]; +} + +- (void)micCpListByUidList:(NSArray *)uidList { + NSString *uidsString = [uidList componentsJoinedByString:@","]; + [Api getRoomMicCpListByUidList:^(BaseModel * _Nullable data, NSInteger code, NSString * _Nullable msg) { + if (code == 200) { + if ([[self getView] respondsToSelector:@selector(getMicCpListByUidListSuccess:)]) { + NSArray *cpList = [MicCpInfoModel modelsWithArray:data.data]; + [[self getView] getMicCpListByUidListSuccess:cpList]; + } + } + } uidList:uidsString]; +} + @end diff --git a/YuMi/Modules/YMRoom/Protocol/XPRoomProtocol.h b/YuMi/Modules/YMRoom/Protocol/XPRoomProtocol.h index 519f3818..a95ee5ea 100644 --- a/YuMi/Modules/YMRoom/Protocol/XPRoomProtocol.h +++ b/YuMi/Modules/YMRoom/Protocol/XPRoomProtocol.h @@ -10,7 +10,7 @@ NS_ASSUME_NONNULL_BEGIN -@class RoomInfoModel, UserInfoModel, NIMChatroom, FirstChargeRoomWindowModel, XPRedPacketModel, BoomInfoModel, BoomDetailModel; +@class RoomInfoModel, UserInfoModel, NIMChatroom, FirstChargeRoomWindowModel, XPRedPacketModel, BoomInfoModel, BoomDetailModel, MicCpInfoModel; @protocol XPRoomProtocol @@ -42,7 +42,13 @@ NS_ASSUME_NONNULL_BEGIN -(void)getKickUserListSuccessWithList:(NSArray *)list; - (void)getRoomBoomInfoSuccess:(NSArray *)models; + - (void)getRoomBoomExplosionSuccess:(BoomInfoModel *)model; + +- (void)getMicCpListByRoomUidSuccess:(NSArray *)cpList; + +- (void)getMicCpListByUidListSuccess:(NSArray *)cpList; + @end NS_ASSUME_NONNULL_END diff --git a/YuMi/Modules/YMRoom/View/FaceView/Presenter/XPRoomFaceTool.m b/YuMi/Modules/YMRoom/View/FaceView/Presenter/XPRoomFaceTool.m index b95efcb6..29ccc06b 100644 --- a/YuMi/Modules/YMRoom/View/FaceView/Presenter/XPRoomFaceTool.m +++ b/YuMi/Modules/YMRoom/View/FaceView/Presenter/XPRoomFaceTool.m @@ -296,8 +296,6 @@ done: self.chatFaceTabList = [ChatFaceResponse modelsWithArray:data]; NSString *cachePath = [self getFaceCachePath]; - NSLog(@"-------------- 表情缓存路径: %@", cachePath); - NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; NSMutableDictionary *cachedUrls = [[defaults objectForKey:@"SVGACachedUrls"] mutableCopy] ?: [NSMutableDictionary dictionary]; @@ -310,7 +308,6 @@ done: // 检查URL是否已缓存且文件存在 if ([cachedUrls[faceVo.faceUrl] isEqualToString:fileName] && [[NSFileManager defaultManager] fileExistsAtPath:filePath]) { - NSLog(@"已有 SVGA文件缓存:%@", faceVo.faceUrl); continue; } @@ -320,7 +317,6 @@ done: cachedUrls[faceVo.faceUrl] = fileName; [defaults setObject:cachedUrls forKey:@"SVGACachedUrls"]; [defaults synchronize]; - NSLog(@"SVGA文件缓存成功:%@ - %@", faceVo.faceUrl, filePath); } }]; } diff --git a/YuMi/Modules/YMRoom/View/StageView/MicMidpointRectManager.h b/YuMi/Modules/YMRoom/View/StageView/MicMidpointRectManager.h index d02ad7f5..f519cd6d 100644 --- a/YuMi/Modules/YMRoom/View/StageView/MicMidpointRectManager.h +++ b/YuMi/Modules/YMRoom/View/StageView/MicMidpointRectManager.h @@ -5,11 +5,12 @@ // Created by AI Assistant on 2024/12/19. // -#import +#import NS_ASSUME_NONNULL_BEGIN @class MicMidpointRectManager; +@class UIView; @protocol MicMidpointRectManagerDelegate @@ -23,6 +24,10 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, weak) id delegate; @property (nonatomic, weak) UIView *containerView; +/// 缓存:当前房间的CP关系列表 +@property (nonatomic, strong, readonly) NSArray *cachedCpList; +/// 缓存:麦位position -> uid(无用户为-1) +@property (nonatomic, strong, readonly) NSDictionary *micPositionToUid; /// 初始化方法 - (instancetype)initWithContainerView:(UIView *)containerView; @@ -38,12 +43,59 @@ NS_ASSUME_NONNULL_BEGIN /// 移除指定位置的中点矩形 - (void)removeMidpointRectAtFrame:(CGRect)frame; -/// 播放指定位置的SVGA动画 +/// 播放指定位置的随机SVGA动画 - (void)playSVGAAnimationAtFrame:(CGRect)frame; +/// 播放指定位置的指定资源名SVGA动画(资源名不带后缀) +- (void)playSVGAAnimationAtFrame:(CGRect)frame withNamed:(NSString *)resourceName; + +/// 抽象:在指定中点位置绘制关系位(透明容器),并根据CP列表与两端uid匹配后播放对应等级SVGA +/// - Parameters: +/// - frame: 中点关系位frame +/// - micPairText: 调试展示用的麦位对文本(仅DEBUG下显示,50%透明) +/// - leftUid: 左侧麦位用户uid(0表示无) +/// - rightUid: 右侧麦位用户uid(0表示无) +/// - cpList: CP关系列表(MicCpInfoModel 数组) +- (void)addRelationshipAtFrame:(CGRect)frame + micPairText:(NSString *)micPairText + leftUid:(NSInteger)leftUid + rightUid:(NSInteger)rightUid + cpList:(NSArray *)cpList; + +/// 基于用户UID清理相关的中点矩形和SVGA动画 +/// - Parameter uids: 需要清理的用户UID数组 +- (void)removeMidpointRectsForUids:(NSArray *)uids; + +/// 设置并缓存CP列表 +- (void)setCpListCache:(NSArray *)cpList; + +/// 使用当前stageView重建麦位快照(position->uid),无用户记为-1 +- (void)rebuildMicSnapshotWithStageView:(id)stageView micCount:(NSInteger)micCount; + +/// 基于上一次快照与当前stageView的实际用户,生成“变动用户+左右邻居”的uid列表(字符串数组)并更新快照 +- (NSArray *)uidListForChangedMicWithStageView:(id)stageView micCount:(NSInteger)micCount; + +/// 计算本次与快照的差异,返回 @{ @"added": NSArray, @"removed": NSArray } 并更新快照 +- (NSDictionary *> *)diffMicChangeWithStageView:(id)stageView micCount:(NSInteger)micCount; + +/// 从缓存中移除包含这些uid的所有CP关系 +- (void)removeCpEntriesForUids:(NSArray *)uids; + +/// 用新列表替换指定uid相关的CP关系(先移除涉及这些uid的旧数据,再合并新数据) +- (void)mergeCpListCache:(NSArray *)cpList replaceForUids:(NSArray *)uids; + /// 停止所有SVGA动画 - (void)stopAllSVGAAnimations; +/// 处理下麦事件:移除相关用户的CP关系和SVGA显示 +/// - Parameters: +/// - downMicUids: 下麦用户的UID数组 +/// - stageView: 当前舞台视图 +/// - micCount: 麦位总数 +- (void)handleDownMicEvent:(NSArray *)downMicUids + stageView:(id)stageView + micCount:(NSInteger)micCount; + @end NS_ASSUME_NONNULL_END diff --git a/YuMi/Modules/YMRoom/View/StageView/MicMidpointRectManager.m b/YuMi/Modules/YMRoom/View/StageView/MicMidpointRectManager.m index d97a5753..5ca18094 100644 --- a/YuMi/Modules/YMRoom/View/StageView/MicMidpointRectManager.m +++ b/YuMi/Modules/YMRoom/View/StageView/MicMidpointRectManager.m @@ -6,13 +6,22 @@ // #import "MicMidpointRectManager.h" +#import "MicCpInfoModel.h" #import +#import "StageView.h" +#import + +@class SVGAImageView; +@class SVGAParser; @interface MicMidpointRectManager () @property (nonatomic, strong) NSMutableArray *midpointRects; @property (nonatomic, strong) NSMutableDictionary *svgaViews; @property (nonatomic, strong) SVGAParser *svgaParser; +@property (nonatomic, strong) NSArray *cachedCpListInternal; +@property (nonatomic, strong) NSMutableDictionary *micPositionToUidInternal; // position->uid (-1 if empty) +@property (nonatomic, strong) NSMutableDictionary *midpointRectInfo; // frameKey -> @{@"leftUid": @(uid), @"rightUid": @(uid)} @end @@ -24,31 +33,39 @@ _midpointRects = [NSMutableArray array]; _svgaViews = [NSMutableDictionary dictionary]; _svgaParser = [[SVGAParser alloc] init]; + _micPositionToUidInternal = [NSMutableDictionary dictionary]; + _midpointRectInfo = [NSMutableDictionary dictionary]; } return self; } +- (NSArray *)cachedCpList { + return self.cachedCpListInternal ?: @[]; +} + +- (NSDictionary *)micPositionToUid { + return self.micPositionToUidInternal ?: @{}; +} + - (void)addMidpointRectAtFrame:(CGRect)frame micPairText:(NSString *)micPairText autoPlaySVGA:(BOOL)autoPlaySVGA { - - // 创建背景矩形 + // 创建透明容器 UIView *rectView = [[UIView alloc] initWithFrame:frame]; - rectView.backgroundColor = [[UIColor blueColor] colorWithAlphaComponent:0.3]; - rectView.layer.borderColor = [UIColor blueColor].CGColor; - rectView.layer.borderWidth = 2.0; - rectView.layer.cornerRadius = 8.0; + rectView.backgroundColor = [UIColor clearColor]; rectView.userInteractionEnabled = NO; rectView.tag = 56002; - // 添加标签显示麦位对 +#if DEBUG + // 仅在DEBUG显示麦位对文本,50%透明 UILabel *label = [[UILabel alloc] init]; label.text = micPairText; - label.textColor = [UIColor whiteColor]; + label.textColor = [[UIColor whiteColor] colorWithAlphaComponent:0.5]; label.font = [UIFont boldSystemFontOfSize:12]; label.textAlignment = NSTextAlignmentCenter; label.frame = rectView.bounds; [rectView addSubview:label]; +#endif // 添加到容器视图 [self.containerView addSubview:rectView]; @@ -62,6 +79,156 @@ NSLog(@"🔧 添加中点矩形: %@, frame: %@", micPairText, NSStringFromCGRect(frame)); } +- (void)addRelationshipAtFrame:(CGRect)frame + micPairText:(NSString *)micPairText + leftUid:(NSInteger)leftUid + rightUid:(NSInteger)rightUid + cpList:(NSArray *)cpList { + // 基础容器 + [self addMidpointRectAtFrame:frame micPairText:micPairText autoPlaySVGA:NO]; + + // 存储中点矩形的用户信息 + NSString *frameKey = NSStringFromCGRect(frame); + self.midpointRectInfo[frameKey] = @{ + @"leftUid": @(leftUid), + @"rightUid": @(rightUid) + }; + + if (leftUid <= 0 || rightUid <= 0 || cpList.count == 0) { + return; + } + // 遍历匹配并播放对应等级的SVGA + for (MicCpInfoModel *obj in cpList) { + BOOL match = (obj.uid == leftUid && obj.loverUid == rightUid) || (obj.uid == rightUid && obj.loverUid == leftUid); + if (match) { + NSInteger safeLevel = MAX(1, MIN(5, obj.cpLevel+1)); + NSString *svgaName = [NSString stringWithFormat:@"mic_cp_lv%ld", (long)safeLevel]; + [self playSVGAAnimationAtFrame:frame withNamed:svgaName]; + break; + } + } +} + +#pragma mark - Cache & Snapshot + +- (void)setCpListCache:(NSArray *)cpList { + self.cachedCpListInternal = cpList ?: @[]; +} + +- (void)rebuildMicSnapshotWithStageView:(id)stageView micCount:(NSInteger)micCount { + [self.micPositionToUidInternal removeAllObjects]; + for (NSInteger i = 0; i < micCount; i++) { + NSInteger uid = -1; + if ([stageView respondsToSelector:@selector(findMicroViewByIndex:)]) { + UIView *microView = [(StageView *)stageView findMicroViewByIndex:i]; + if ([microView respondsToSelector:@selector(getUser)]) { + id userInfo = [microView performSelector:@selector(getUser)]; + if (userInfo && [userInfo respondsToSelector:@selector(uid)]) { + uid = (NSInteger)[userInfo valueForKey:@"uid"]; + if (uid <= 0) { uid = -1; } + } + } + } + self.micPositionToUidInternal[@(i)] = @(uid); + } +} + +- (NSArray *)uidListForChangedMicWithStageView:(id)stageView micCount:(NSInteger)micCount { + NSMutableSet *result = [NSMutableSet set]; + // 计算新状态 + NSMutableArray *newUids = [NSMutableArray arrayWithCapacity:micCount]; + for (NSInteger i = 0; i < micCount; i++) { + NSInteger uid = -1; + if ([stageView respondsToSelector:@selector(findMicroViewByIndex:)]) { + UIView *microView = [(StageView *)stageView findMicroViewByIndex:i]; + if ([microView respondsToSelector:@selector(getUser)]) { + id userInfo = [microView performSelector:@selector(getUser)]; + if (userInfo && [userInfo respondsToSelector:@selector(uid)]) { + uid = (NSInteger)[userInfo valueForKey:@"uid"]; + if (uid <= 0) { uid = -1; } + } + } + } + [newUids addObject:@(uid)]; + NSNumber *oldVal = self.micPositionToUidInternal[@(i)] ?: @(-1); + if (oldVal.integerValue != uid) { + // 该坑位发生变化,将该位及其左右邻居加入 + if (uid > 0) { [result addObject:@(uid)]; } + NSInteger leftIdx = i - 1; + NSInteger rightIdx = i + 1; + if (leftIdx >= 0) { + NSInteger leftUid = (leftIdx < newUids.count) ? newUids[leftIdx].integerValue : -1; + if (leftUid > 0) { [result addObject:@(leftUid)]; } + } + if (rightIdx < micCount) { + NSInteger rightUid = (rightIdx < newUids.count) ? newUids[rightIdx].integerValue : -1; + if (rightUid > 0) { [result addObject:@(rightUid)]; } + } + } + } + // 更新快照 + for (NSInteger i = 0; i < micCount; i++) { + self.micPositionToUidInternal[@(i)] = newUids[i]; + } + // 转字符串数组 + NSMutableArray *uids = [NSMutableArray arrayWithCapacity:result.count]; + for (NSNumber *num in result) { [uids addObject:num.stringValue]; } + return uids; +} + +- (NSDictionary *> *)diffMicChangeWithStageView:(id)stageView micCount:(NSInteger)micCount { + NSMutableArray *added = [NSMutableArray array]; + NSMutableArray *removed = [NSMutableArray array]; + for (NSInteger i = 0; i < micCount; i++) { + NSInteger uid = -1; + if ([stageView respondsToSelector:@selector(findMicroViewByIndex:)]) { + UIView *microView = [(StageView *)stageView findMicroViewByIndex:i]; + if ([microView respondsToSelector:@selector(getUser)]) { + id userInfo = [microView performSelector:@selector(getUser)]; + if (userInfo && [userInfo respondsToSelector:@selector(uid)]) { + uid = (NSInteger)[userInfo valueForKey:@"uid"]; + if (uid <= 0) { uid = -1; } + } + } + } + NSInteger oldUid = (self.micPositionToUidInternal[@(i)] ?: @(-1)).integerValue; + if (uid != oldUid) { + if (oldUid > 0 && uid == -1) { // 下麦 + [removed addObject:@(oldUid)]; + } else if (uid > 0) { // 上麦或换位到此 + [added addObject:@(uid)]; + } + } + self.micPositionToUidInternal[@(i)] = @(uid); + } + return @{ @"added": added.copy, @"removed": removed.copy }; +} + +- (void)removeCpEntriesForUids:(NSArray *)uids { + if (uids.count == 0 || self.cachedCpListInternal.count == 0) { return; } + NSMutableArray *filtered = [NSMutableArray array]; + NSSet *uidSet = [NSSet setWithArray:uids]; + for (id obj in self.cachedCpListInternal) { + NSInteger a = 0, b = 0; + if ([obj respondsToSelector:@selector(uid)]) { a = (NSInteger)[obj valueForKey:@"uid"]; } + if ([obj respondsToSelector:@selector(loverUid)]) { b = (NSInteger)[obj valueForKey:@"loverUid"]; } + if (![uidSet containsObject:@(a)] && ![uidSet containsObject:@(b)]) { + [filtered addObject:obj]; + } + } + self.cachedCpListInternal = filtered.copy; +} + +- (void)mergeCpListCache:(NSArray *)cpList replaceForUids:(NSArray *)uids { + // 先移除相关uid的旧数据 + [self removeCpEntriesForUids:uids]; + if (cpList.count == 0) { return; } + // 合并新数据 + NSMutableArray *merged = [self.cachedCpListInternal mutableCopy] ?: [NSMutableArray array]; + [merged addObjectsFromArray:cpList]; + self.cachedCpListInternal = merged.copy; +} + - (void)removeAllMidpointRects { // 停止所有SVGA动画 [self stopAllSVGAAnimations]; @@ -71,6 +238,7 @@ [rectView removeFromSuperview]; } [self.midpointRects removeAllObjects]; + [self.midpointRectInfo removeAllObjects]; NSLog(@"🔧 移除所有中点矩形"); } @@ -94,6 +262,9 @@ } } + // 移除对应的用户信息 + [self.midpointRectInfo removeObjectForKey:frameKey]; + NSLog(@"🔧 移除指定位置的中点矩形: %@", NSStringFromCGRect(frame)); } @@ -101,40 +272,35 @@ // 随机选择一个SVGA资源 NSArray *svgaFiles = @[@"mic_cp_lv1", @"mic_cp_lv2", @"mic_cp_lv3", @"mic_cp_lv4", @"mic_cp_lv5"]; NSString *randomSVGA = svgaFiles[arc4random_uniform((uint32_t)svgaFiles.count)]; - - // 构建SVGA文件路径 - NSString *svgaPath = [[NSBundle mainBundle] pathForResource:randomSVGA ofType:@"svga"]; + [self playSVGAAnimationAtFrame:frame withNamed:randomSVGA]; +} + +// 播放指定资源名SVGA +- (void)playSVGAAnimationAtFrame:(CGRect)frame withNamed:(NSString *)resourceName { + if (resourceName.length == 0) { return; } + NSString *svgaPath = [[NSBundle mainBundle] pathForResource:resourceName ofType:@"svga"]; if (!svgaPath) { - NSLog(@"⚠️ 找不到SVGA文件: %@", randomSVGA); + NSLog(@"⚠️ 找不到SVGA文件: %@", resourceName); return; } - - // 创建SVGAImageView SVGAImageView *svgaView = [[SVGAImageView alloc] initWithFrame:frame]; svgaView.contentMode = UIViewContentModeScaleAspectFit; svgaView.userInteractionEnabled = NO; svgaView.backgroundColor = [UIColor clearColor]; - - // 添加到容器视图 [self.containerView addSubview:svgaView]; - - // 保存SVGA视图引用 NSString *frameKey = NSStringFromCGRect(frame); self.svgaViews[frameKey] = svgaView; - - // 解析并播放SVGA动画 - [self.svgaParser parseWithURL:[NSURL fileURLWithPath:svgaPath] + [self.svgaParser parseWithURL:[NSURL fileURLWithPath:svgaPath] completionBlock:^(SVGAVideoEntity * _Nonnull videoItem) { dispatch_async(dispatch_get_main_queue(), ^{ svgaView.videoItem = videoItem; - svgaView.loops = 0; + svgaView.loops = 0; svgaView.clearsAfterStop = YES; [svgaView startAnimation]; - - NSLog(@"🎬 开始播放SVGA动画: %@, frame: %@", randomSVGA, NSStringFromCGRect(frame)); + NSLog(@"🎬 开始播放SVGA动画: %@, frame: %@", resourceName, NSStringFromCGRect(frame)); }); } failureBlock:^(NSError * _Nonnull error) { - NSLog(@"❌ SVGA动画解析失败: %@, error: %@", randomSVGA, error.localizedDescription); + NSLog(@"❌ SVGA动画解析失败: %@, error: %@", resourceName, error.localizedDescription); [svgaView removeFromSuperview]; [self.svgaViews removeObjectForKey:frameKey]; }]; @@ -150,6 +316,60 @@ NSLog(@"🔧 停止所有SVGA动画"); } +- (void)removeMidpointRectsForUids:(NSArray *)uids { + if (uids.count == 0) { + NSLog(@"🔧 基于UID清理:没有需要清理的用户,跳过处理"); + return; + } + + NSLog(@"🔧 基于UID清理:开始清理用户 %@ 相关的中点矩形和SVGA", uids); + + NSSet *uidSet = [NSSet setWithArray:uids]; + NSMutableArray *frameKeysToRemove = [NSMutableArray array]; + + // 遍历所有中点矩形信息,找出涉及下麦用户的矩形 + for (NSString *frameKey in self.midpointRectInfo.allKeys) { + NSDictionary *rectInfo = self.midpointRectInfo[frameKey]; + NSNumber *leftUid = rectInfo[@"leftUid"]; + NSNumber *rightUid = rectInfo[@"rightUid"]; + + // 检查是否涉及下麦用户 + if ([uidSet containsObject:leftUid] || [uidSet containsObject:rightUid]) { + [frameKeysToRemove addObject:frameKey]; + } + } + + // 移除相关的中点矩形和SVGA动画 + for (NSString *frameKey in frameKeysToRemove) { + CGRect frame = CGRectFromString(frameKey); + [self removeMidpointRectAtFrame:frame]; + } + + NSLog(@"🔧 基于UID清理完成:移除了 %lu 个中点矩形和SVGA动画", (unsigned long)frameKeysToRemove.count); +} + +- (void)handleDownMicEvent:(NSArray *)downMicUids + stageView:(id)stageView + micCount:(NSInteger)micCount { + if (downMicUids.count == 0) { + NSLog(@"🔧 处理下麦事件:没有下麦用户,跳过处理"); + return; + } + + NSLog(@"🔧 处理下麦事件:下麦用户 %@", downMicUids); + + // 1. 从缓存中移除相关用户的CP关系 + [self removeCpEntriesForUids:downMicUids]; + + // 2. 基于UID精确清理相关的中点矩形和SVGA动画 + [self removeMidpointRectsForUids:downMicUids]; + + // 3. 更新麦位快照 + [self rebuildMicSnapshotWithStageView:stageView micCount:micCount]; + + NSLog(@"🔧 下麦事件处理完成"); +} + #pragma mark - Dealloc - (void)dealloc { diff --git a/YuMi/Modules/YMRoom/View/StageView/MicroView/MicroView.m b/YuMi/Modules/YMRoom/View/StageView/MicroView/MicroView.m index a406ce4b..7f5188ab 100644 --- a/YuMi/Modules/YMRoom/View/StageView/MicroView/MicroView.m +++ b/YuMi/Modules/YMRoom/View/StageView/MicroView/MicroView.m @@ -677,7 +677,6 @@ NSString * headWearUrl = userInfo.headwearEffect.length ? userInfo.headwearEffect : userInfo.headWearUrl.length ? userInfo.headWearUrl : userInfo.headwearPic; if (headWearUrl.length > 0 && !userInfo.vipMic) { if ([userInfo isHeadWearSVGA]) { - NSLog(@"🎮 MicroView: 配置 SVGA 头饰: %@", headWearUrl); self.headWearSVGAImageView.hidden = NO; SVGAParser *parse = [[SVGAParser alloc] init]; @kWeakify(self); @@ -690,22 +689,18 @@ } } failureBlock:^(NSError * _Nullable error) { }]; } else { - NSLog(@"🎮 MicroView: 配置精灵图头饰: %@", headWearUrl); self.headWearImageView.hidden = NO; NSURL *url = [NSURL URLWithString:headWearUrl]; @kWeakify(self); [self.manager loadSpriteSheetImageWithURL:url completionBlock:^(YYSpriteSheetImage * _Nullable sprit) { @kStrongify(self); self.headWearImageView.image = sprit; - NSLog(@"🎮 MicroView: 精灵图头饰加载完成,应用 turbo mode 状态"); // 加载完成后应用 turbo mode 状态 [self applyTurboModeToHeadWear]; } failureBlock:^(NSError * _Nullable error) { - NSLog(@"🎮 MicroView: 精灵图头饰加载失败: %@", error.localizedDescription); }]; } } else { - NSLog(@"🎮 MicroView: 隐藏头饰 (URL为空或VIP麦位)"); self.headWearSVGAImageView.hidden = YES; self.headWearImageView.hidden = YES; } @@ -827,23 +822,16 @@ - (void)handleTurboModeStateChanged:(NSNotification *)notification { NSDictionary *userInfo = notification.userInfo; BOOL enabled = [userInfo[@"enabled"] boolValue]; - - NSLog(@"🎮 MicroView: 收到 turbo mode 状态变化通知,新状态: %@", enabled ? @"开启" : @"关闭"); - + self.isTurboModeEnabled = enabled; // 如果当前有头饰,需要重新配置以应用新的 turbo mode 状态 if (self.userInfo && (self.userInfo.headwearEffect.length || self.userInfo.headWearUrl.length || self.userInfo.headwearPic.length)) { - NSLog(@"🎮 MicroView: 当前有头饰,更新 turbo mode 状态"); [self updateHeadWearForTurboMode]; - } else { - NSLog(@"🎮 MicroView: 当前无头饰,跳过 turbo mode 状态更新"); } } - (void)updateHeadWearForTurboMode { - NSLog(@"🎮 MicroView: 更新头饰 turbo mode 状态,当前状态: %@", self.isTurboModeEnabled ? @"开启" : @"关闭"); - if (self.isTurboModeEnabled) { // Turbo mode 开启:只显示第一帧,不播放动画 [self setHeadWearToFirstFrameOnly]; @@ -853,12 +841,9 @@ } } -- (void)setHeadWearToFirstFrameOnly { - NSLog(@"🎮 MicroView: 设置头饰为只显示第一帧模式"); - +- (void)setHeadWearToFirstFrameOnly { // 对于 YYAnimatedImageView,停止动画并显示第一帧 if (!self.headWearImageView.hidden) { - NSLog(@"🎮 MicroView: 停止 YYAnimatedImageView 动画"); [self.headWearImageView stopAnimating]; UIImage *sprites = (YYSpriteSheetImage *)self.headWearImageView.image; self.tempSprites = sprites; @@ -866,36 +851,28 @@ if (firstFrame) { self.headWearImageView.image = firstFrame; } - } else - - // 对于 SVGAImageView,停止动画 - if (!self.headWearSVGAImageView.hidden) { - NSLog(@"🎮 MicroView: 停止 SVGAImageView 动画"); + } else if (!self.headWearSVGAImageView.hidden) { + // 对于 SVGAImageView,停止动画 [self.headWearSVGAImageView pauseAnimation]; // [self.headWearSVGAImageView stepToFrame:1 andPlay:NO]; } } - (void)setHeadWearToNormalPlayback { - NSLog(@"🎮 MicroView: 设置头饰为正常播放模式"); // 对于 YYAnimatedImageView,恢复动画播放 if (!self.headWearImageView.hidden && self.headWearImageView.image) { - NSLog(@"🎮 MicroView: 恢复 YYAnimatedImageView 动画播放"); [self.headWearImageView startAnimating]; } // 对于 SVGAImageView,恢复动画播放 if (!self.headWearSVGAImageView.hidden && self.headWearSVGAImageView.videoItem) { - NSLog(@"🎮 MicroView: 恢复 SVGAImageView 动画播放"); self.headWearImageView.image = self.tempSprites; [self.headWearSVGAImageView startAnimation]; } } - (void)applyTurboModeToHeadWear { - NSLog(@"🎮 MicroView: 应用 turbo mode 到头饰,当前状态: %@", self.isTurboModeEnabled ? @"开启" : @"关闭"); - if (self.isTurboModeEnabled) { // Turbo mode 开启:只显示第一帧,不播放动画 [self setHeadWearToFirstFrameOnly]; diff --git a/YuMi/Modules/YMRoom/View/XPRoomViewController.m b/YuMi/Modules/YMRoom/View/XPRoomViewController.m index ecf3929a..36a07b08 100644 --- a/YuMi/Modules/YMRoom/View/XPRoomViewController.m +++ b/YuMi/Modules/YMRoom/View/XPRoomViewController.m @@ -94,6 +94,10 @@ #import "RoomResourceManager.h" #import "LuckyPackageLogicManager.h" +#import "MicCpInfoModel.h" +#import "XPMessageRemoteExtModel.h" +#import "MicMidpointRectManager.h" + // 🔧 新增:Turbo Mode Tips 相关 #import "XPTurboModeTipsManager.h" #import "BuglyManager.h" @@ -194,6 +198,10 @@ XPCandyTreeInsufficientBalanceViewDelegate> ///是否正在显示红包弹窗,防止显示多个弹窗 @property(nonatomic,assign) BOOL isShowRedPacket; @property(nonatomic,copy) NSString *releaseCoins; + +// 🔧 防护机制:房间状态标志位 +@property (atomic, assign) BOOL isExitingRoom; // 是否正在退出房间 +@property (atomic, assign) BOOL isViewActive; // VC 是否可见/活跃 @property(nonatomic,copy) NSString *myCoins; @property(nonatomic,strong) UIButton *exitGameButton; @@ -204,6 +212,13 @@ XPCandyTreeInsufficientBalanceViewDelegate> /// 🔧 修复:保存 block 形式的通知观察者,防止内存泄漏 @property(nonatomic,strong) id exchangeRoomAnimationViewObserver; +/// 当前房间的CP关系列表(用于在中点关系位播放对应cp等级的SVGA) +@property (nonatomic, strong) NSArray *currentCpList; +@property (nonatomic, assign) BOOL currentUserMicStatusChanged; // 当前用户麦位状态是否发生变化(上麦、下麦、换位) +@property (nonatomic, assign) BOOL currentUserWasOnMic; // 当前用户之前是否在麦上 +@property (nonatomic, assign) NSInteger currentUserMicPosition; // 当前用户的麦位位置(-1表示不在麦上) +@property (nonatomic, assign) BOOL hasCompletedRoomInitialization; // 是否已完成进房初始化 + @end @implementation XPRoomViewController @@ -296,6 +311,13 @@ XPCandyTreeInsufficientBalanceViewDelegate> NSLog(@"🔄 XPRoomViewController: 开始销毁"); + // 🔧 防护:强制清理 TRTC 连接,防止资源泄漏 + [[RtcManager instance] exitRoom]; + + // 🔧 防护:标记房间正在退出,防止后续异步回调 + self.isExitingRoom = YES; + self.isViewActive = NO; + [[RoomBoomManager sharedManager] leaveRoom]; [XPSkillCardPlayerManager shareInstance].photoIdList = nil; @@ -552,6 +574,15 @@ XPCandyTreeInsufficientBalanceViewDelegate> [super viewWillDisappear:animated]; self.freeView.hidden = YES; + // 🔧 防护:标记 VC 即将不可见 + self.isViewActive = NO; + + // 🔧 防护:如果 VC 被移除或 dismiss,强制清理 TRTC + if (self.isMovingFromParentViewController || self.isBeingDismissed) { + [[RtcManager instance] exitRoom]; + self.isExitingRoom = YES; + } + // 如果连击正在进行,强制重置 if ([[GiftComboManager sharedManager] isActive]) { NSLog(@"📱 房间即将退出,检查连击状态"); @@ -564,6 +595,9 @@ XPCandyTreeInsufficientBalanceViewDelegate> self.navigationController.interactivePopGestureRecognizer.enabled = YES; [XPSkillCardPlayerManager shareInstance].isInRoomVC = NO; + // 🔧 防护:确保标记为不可见 + self.isViewActive = NO; + // 🔧 修复:发送房间退出通知,让 BuglyManager 知道用户已退出房间 [[NSNotificationCenter defaultCenter] postNotificationName:@"RoomDidExit" object:nil @@ -576,6 +610,9 @@ XPCandyTreeInsufficientBalanceViewDelegate> self.navigationController.interactivePopGestureRecognizer.enabled = NO; [XPSkillCardPlayerManager shareInstance].isInRoomVC = YES; + // 🔧 防护:标记 VC 可见 + self.isViewActive = YES; + // 🔧 修复:发送房间进入通知,让 BuglyManager 知道用户已进入房间 [[NSNotificationCenter defaultCenter] postNotificationName:@"RoomDidEnter" object:nil @@ -634,7 +671,9 @@ XPCandyTreeInsufficientBalanceViewDelegate> [self.view addSubview:self.animationView]; // 🔧 新增:添加测试按钮,用于测试 Turbo Mode Tips 弹窗 +#if DEBUG [self addTestButton]; +#endif } - (void)__layoutTwentyMicStage { @@ -1029,6 +1068,13 @@ XPCandyTreeInsufficientBalanceViewDelegate> // 🔧 新增:如果是 SocialStageView,绘制中点矩形 [self drawSocialStageMidpointRects]; + + // 🔧 新增:进房成功后调用CP相关API + [self callMicCpListByRoomUidAfterRoomEntered]; + + // 🔧 新增:stage view类型变化时调用CP相关API + NSMutableDictionary *currentQueue = [self.stageView getMicroQueue]; + [self callMicCpListByUidListOnMicChangeWithQueue:currentQueue]; [self addExitGameButton]; @@ -1362,7 +1408,10 @@ XPCandyTreeInsufficientBalanceViewDelegate> [self.backContainerView onRoomEntered]; [self.littleGameView onRoomEntered]; if ([XPRoomMiniManager shareManager].getRoomInfo.uid != self.roomUid.integerValue) {// 最小化进房 还是原来的房间的话 不需要重新进入云信 因为压根没退 - [self.presenter enterNIMRoom:[NSString stringWithFormat:@"%ld", self.roomInfo.roomId] user:self.userInfo]; + // 🔧 防护:检查是否允许进房 + if (!self.isExitingRoom && self.isViewActive) { + [self.presenter enterNIMRoom:[NSString stringWithFormat:@"%ld", self.roomInfo.roomId] user:self.userInfo]; + } [self.functionView onRoomEntered]; [self.messageContainerView onRoomEntered]; [self.menuContainerView onRoomEntered]; @@ -1437,7 +1486,10 @@ XPCandyTreeInsufficientBalanceViewDelegate> [self.backContainerView onRoomEntered]; [self.littleGameView onRoomEntered]; if ([XPRoomMiniManager shareManager].getRoomInfo.uid != self.roomUid.integerValue) {// 最小化进房 还是原来的房间的话 不需要重新进入云信 因为压根没退 - [self.presenter enterNIMRoom:[NSString stringWithFormat:@"%ld", self.roomInfo.roomId] user:self.userInfo]; + // 🔧 防护:检查是否允许进房 + if (!self.isExitingRoom && self.isViewActive) { + [self.presenter enterNIMRoom:[NSString stringWithFormat:@"%ld", self.roomInfo.roomId] user:self.userInfo]; + } [self.functionView onRoomEntered]; [self.messageContainerView onRoomEntered]; [self.menuContainerView onRoomEntered]; @@ -1661,15 +1713,27 @@ XPCandyTreeInsufficientBalanceViewDelegate> self.quickMessageContainerView.titleArray = self.roomInfo.speakTemplate; - [self.presenter enterNIMRoom:[NSString stringWithFormat:@"%ld", self.roomInfo.roomId] user:self.userInfo]; + // 🔧 防护:检查是否允许进房 + if (!self.isExitingRoom && self.isViewActive) { + [self.presenter enterNIMRoom:[NSString stringWithFormat:@"%ld", self.roomInfo.roomId] user:self.userInfo]; + } } - (void)enterRoomSuccess:(NIMChatroom *)chatRoom { [XNDJTDDLoadingTool hideHUDInView:self.navigationController.view]; + // 🔧 防护:检查 VC 是否仍然有效 + if (self.isExitingRoom || !self.isViewActive) { + NSLog(@"🔧 enterRoomSuccess: VC 已退出或不可见,忽略回调"); + return; + } [self.stageView onRoomEntered]; [self.functionView onRoomEntered]; + + // 🔧 新增:初始化当前用户的麦位状态 + [self initializeCurrentUserMicStatus]; + //上报进房 if (self.roomInfo != nil) { [self.presenter reportUserInterRoom:[NSString stringWithFormat:@"%zd", self.roomInfo.uid]]; @@ -1685,6 +1749,13 @@ XPCandyTreeInsufficientBalanceViewDelegate> - (void)enterRoomFail:(NSInteger)code { [XNDJTDDLoadingTool hideHUDInView:self.navigationController.view]; [self hideHUD]; + + // 🔧 防护:检查 VC 是否仍然有效 + if (self.isExitingRoom || !self.isViewActive) { + NSLog(@"🔧 enterRoomFail: VC 已退出或不可见,忽略回调 (code: %ld)", (long)code); + return; + } + if (code == 13003) { [self showErrorToast:YMLocalizedString(@"XPRoomViewController3")]; } @@ -1851,7 +1922,10 @@ XPCandyTreeInsufficientBalanceViewDelegate> [self.backContainerView onRoomEntered]; [self.littleGameView onRoomEntered]; if ([XPRoomMiniManager shareManager].getRoomInfo.uid != self.roomUid.integerValue) {// 最小化进房 还是原来的房间的话 不需要重新进入云信 因为压根没退 - [self.presenter enterNIMRoom:[NSString stringWithFormat:@"%ld", self.roomInfo.roomId] user:self.userInfo]; + // 🔧 防护:检查是否允许进房 + if (!self.isExitingRoom && self.isViewActive) { + [self.presenter enterNIMRoom:[NSString stringWithFormat:@"%ld", self.roomInfo.roomId] user:self.userInfo]; + } [self.functionView onRoomEntered]; [self.messageContainerView onRoomEntered]; [self.menuContainerView onRoomEntered]; @@ -1882,7 +1956,6 @@ XPCandyTreeInsufficientBalanceViewDelegate> [self.presenter openRoom:title type:type roomPwd:@"" roomDesc:@"" backPic:@"" mgId:self.mgId]; } } else { // 当前用户进入别人房间 - // TODO: 房主已经下线。 [self showSuccessToast:YMLocalizedString(@"XPRoomViewController7")]; [self enterRoomFail:0]; } @@ -1994,21 +2067,9 @@ XPCandyTreeInsufficientBalanceViewDelegate> } if (![message.session.sessionId isEqualToString:@(self.roomInfo.roomId).stringValue]) { -// if (message.messageType == NIMMessageTypeCustom) { -// NIMCustomObject *obj = (NIMCustomObject *)message.messageObject; -// if ([obj.attachment isKindOfClass:[AttachmentModel class]]) { -// AttachmentModel *att = (AttachmentModel *)obj.attachment; -// if (!att.isFromPublic) { -// NSLog(@"[Recv] ⛔️ 过滤:房间不匹配 | msg.sid=%@ | curRoomId=%@", -// message.session.sessionId, @(self.roomInfo.roomId).stringValue); -// continue; -// } -// } -// }else { - NSLog(@"[Recv] ⛔️ 过滤:房间不匹配 | msg.sid=%@ | curRoomId=%@", - message.session.sessionId, @(self.roomInfo.roomId).stringValue); - continue; -// } + NSLog(@"[Recv] ⛔️ 过滤:房间不匹配 | msg.sid=%@ | curRoomId=%@", + message.session.sessionId, @(self.roomInfo.roomId).stringValue); + continue; } NSLog(@"[Recv] --- Message Raw Attach Content: %@, %@, %ld", @(message.senderClientType), message.rawAttachContent, (long)message.messageType); @@ -2021,20 +2082,6 @@ XPCandyTreeInsufficientBalanceViewDelegate> if (message.messageType == NIMMessageTypeNotification) { [self handleNIMNotificationTypeMessage:message]; } else if (message.messageType == NIMMessageTypeCustom) { - // 自定义消息排查日志:first/second/size3 -#if DEBUG - if ([message.messageObject isKindOfClass:[NIMCustomObject class]]) { - NIMCustomObject *obj = (NIMCustomObject *)message.messageObject; - if ([obj.attachment isKindOfClass:[AttachmentModel class]]) { - AttachmentModel *att = (AttachmentModel *)obj.attachment; - NSData *payloadJSON = nil; - @try { payloadJSON = [NSJSONSerialization dataWithJSONObject:att.data ?: @{} options:0 error:nil]; } @catch (__unused NSException *e) {} - NSLog(@"[Recv] 🎯 自定义消息 | first=%ld second=%ld | payload=%lub | sid=%@ | ts=%.3f", - (long)att.first, (long)att.second, (unsigned long)payloadJSON.length, - message.session.sessionId, [[NSDate date] timeIntervalSince1970]); - } - } -#endif [self handleNimCustomTypeMessage:message]; } else if(message.messageType == NIMMessageTypeText) { [self.messageContainerView handleNIMTextMessage:message]; @@ -2158,7 +2205,6 @@ XPCandyTreeInsufficientBalanceViewDelegate> //房间类型是否变更了(从个播->普通,个播->小游戏等) newRoomInfo.hadChangeRoomType = self.roomInfo.type != newRoomInfo.type; BOOL anchorToOther = newRoomInfo.type != RoomType_Anchor && self.roomInfo.type == RoomType_Anchor;//个播变其他房 - RoomType currentType = self.roomInfo.type; self.roomInfo = newRoomInfo; [self.backContainerView onRoomUpdate]; @@ -2424,10 +2470,78 @@ XPCandyTreeInsufficientBalanceViewDelegate> break; } break; + + case MicRelationship_Type: + switch (attachment.second) { + case MicRelationship_CP: + [self handleMicRelationshipCPMessage:attachment]; + break; + } + break; } } } +/// 处理麦位关系CP消息 +- (void)handleMicRelationshipCPMessage:(AttachmentModel *)attachment { + NSLog(@"🔧 接收到麦位关系CP消息"); + + if (!attachment.data || ![attachment.data isKindOfClass:[NSString class]]) { + NSLog(@"⚠️ 麦位关系CP消息:data格式错误,跳过处理"); + return; + } + + NSString *jsonString = (NSString *)attachment.data; + NSData *jsonData = [jsonString dataUsingEncoding:NSUTF8StringEncoding]; + + NSError *error = nil; + NSDictionary *jsonDict = [NSJSONSerialization JSONObjectWithData:jsonData options:0 error:&error]; + if (error) { + NSLog(@"❌ 麦位关系CP消息:JSON解析失败 - %@", error.localizedDescription); + return; + } + + // 验证消息格式 + NSNumber *first = jsonDict[@"first"]; + NSNumber *second = jsonDict[@"second"]; + NSArray *dataArray = jsonDict[@"data"]; + + if (!first || !second || ![dataArray isKindOfClass:[NSArray class]]) { + NSLog(@"⚠️ 麦位关系CP消息:消息格式错误,跳过处理"); + return; + } + + if (first.integerValue != MicRelationship_Type || second.integerValue != MicRelationship_CP) { + NSLog(@"⚠️ 麦位关系CP消息:消息类型不匹配,跳过处理"); + return; + } + + // 解析CP数据 + NSMutableArray *cpList = [NSMutableArray array]; + for (NSDictionary *cpDict in dataArray) { + if ([cpDict isKindOfClass:[NSDictionary class]]) { + MicCpInfoModel *cpInfo = [[MicCpInfoModel alloc] init]; + cpInfo.uid = [cpDict[@"uid"] integerValue]; + cpInfo.loverUid = [cpDict[@"loverUid"] integerValue]; + cpInfo.cpLevel = [cpDict[@"cpLevel"] integerValue]; + [cpList addObject:cpInfo]; + } + } + + NSLog(@"🔧 麦位关系CP消息:解析到 %lu 条CP数据", (unsigned long)cpList.count); + + // 更新当前CP列表 + self.currentCpList = cpList.copy; + + // 更新 MicMidpointRectManager 的缓存 + [self updateMicMidpointRectManagerCache:cpList]; + + // 刷新CP SVGA显示 + [self drawSocialStageMidpointRects]; + + NSLog(@"🔧 麦位关系CP消息处理完成"); +} + - (void)handleUpMicAsk:(AttachmentModel *)attachment { NSNumber *targetUid = [attachment.data objectForKey:@"targetUid"]; if (!targetUid || ![targetUid.stringValue isEqualToString:[AccountInfoStorage instance].getUid]) { @@ -2731,6 +2845,9 @@ XPCandyTreeInsufficientBalanceViewDelegate> - (void)exitRoom { [XNDJTDDLoadingTool showLoading]; + // 🔧 防护:标记正在退出房间 + self.isExitingRoom = YES; + [XPSkillCardPlayerManager shareInstance].micState = MICState_None; [self.stageView exitNIMRoom]; @@ -2849,7 +2966,14 @@ XPCandyTreeInsufficientBalanceViewDelegate> [self.littleGameView destroyMG]; } [[XPRoomMiniManager shareManager] resetLocalMessage]; + + // 🔧 防护:确保 TRTC 退出 [[RtcManager instance] exitRoom]; + + // 🔧 防护:标记房间已退出 + self.isExitingRoom = YES; + self.isViewActive = NO; + [self.presenter exitNIMRoom:[NSString stringWithFormat:@"%ld", self.roomInfo.roomId]]; [self.presenter reportUserOutRoom:[NSString stringWithFormat:@"%ld", self.roomInfo.uid]]; [self handleFirstOutRoom]; @@ -2946,19 +3070,31 @@ XPCandyTreeInsufficientBalanceViewDelegate> [self.functionView onRoomUpdate]; [self.functionView onMicroQueueUpdate:queue]; - BOOL isOnMic = false; - for (MicroQueueModel * info in queue.allValues) { - if (info.userInfo.uid > 0 && [AccountInfoStorage instance].getUid.integerValue == info.userInfo.uid) { - isOnMic = YES; - break; - } - } + // 更新当前用户的麦位状态 + [self updateCurrentUserMicStatus:queue]; + + // 获取当前用户麦位状态 + NSDictionary *currentStatus = [self getCurrentUserMicStatus:queue]; + BOOL isOnMic = [currentStatus[@"isOnMic"] boolValue]; if (isOnMic) { self.anchorScrollView.scrollEnabled = NO; } else { self.anchorScrollView.scrollEnabled = YES; } + + // 🔧 新增:只有在完成进房初始化后才调用mic位变动API + if (self.hasCompletedRoomInitialization) { + // 检测下麦用户并处理CP关系缓存 + [self handleDownMicEventIfNeeded:queue]; + + // 🔧 :更新麦位快照 + [self updateMicMidpointRectManagerSnapshot]; + + [self callMicCpListByUidListOnMicChangeWithQueue:queue]; + } else { + NSLog(@"🔧 进房初始化中,跳过 micCpListByUidList 调用"); + } } - (CGPoint)animationPointAtStageViewByUid:(NSString *)uid { @@ -2978,6 +3114,28 @@ XPCandyTreeInsufficientBalanceViewDelegate> [[RoomBoomManager sharedManager] receiveEnterRoomBoom:model]; } +- (void)getMicCpListByRoomUidSuccess:(NSArray *)cpList { + self.currentCpList = cpList; + // 刷新绘制,按CP关系播放对应SVGA + [self drawSocialStageMidpointRects]; +} + +- (void)getMicCpListByUidListSuccess:(NSArray *)cpList { + self.currentCpList = cpList; + + // 🔧 :更新 MicMidpointRectManager 的缓存 + [self updateMicMidpointRectManagerCache:cpList]; + + // 刷新绘制,按CP关系播放对应SVGA + [self drawSocialStageMidpointRects]; + + // 如果当前用户麦位状态发生变化(上麦、下麦、换位),发送 NIM message + if (self.currentUserMicStatusChanged) { + [self sendMicRelationshipNIMessage:cpList]; + self.currentUserMicStatusChanged = NO; // 重置标志 + } +} + #pragma mark - 首次退出非自己的房间,处理是否需要弹新用户充值优惠 - (void)handleFirstOutRoom { NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; @@ -3056,7 +3214,10 @@ XPCandyTreeInsufficientBalanceViewDelegate> [self.littleGameView onRoomEntered]; [self.littleGameView onRoomEntered]; if ([XPRoomMiniManager shareManager].getRoomInfo.uid != self.roomUid.integerValue) {// 最小化进房 还是原来的房间的话 不需要重新进入云信 因为压根没退 - [self.presenter enterNIMRoom:[NSString stringWithFormat:@"%ld", self.roomInfo.roomId] user:self.userInfo]; + // 🔧 防护:检查是否允许进房 + if (!self.isExitingRoom && self.isViewActive) { + [self.presenter enterNIMRoom:[NSString stringWithFormat:@"%ld", self.roomInfo.roomId] user:self.userInfo]; + } [self.functionView onRoomEntered]; [self.messageContainerView onRoomEntered]; [self.menuContainerView onRoomEntered]; @@ -3080,7 +3241,10 @@ XPCandyTreeInsufficientBalanceViewDelegate> [self.backContainerView onRoomEntered]; [self.littleGameView onRoomEntered]; [self.functionView onRoomEntered]; - [self.presenter enterNIMRoom:[NSString stringWithFormat:@"%ld", self.roomInfo.roomId] user:self.userInfo]; + // 🔧 防护:检查是否允许进房 + if (!self.isExitingRoom && self.isViewActive) { + [self.presenter enterNIMRoom:[NSString stringWithFormat:@"%ld", self.roomInfo.roomId] user:self.userInfo]; + } [self.messageContainerView onRoomEntered]; [self.littleGameView onRoomEntered]; [[XPRoomMiniManager shareManager] configRoomInfo:nil]; @@ -3320,13 +3484,6 @@ XPCandyTreeInsufficientBalanceViewDelegate> return; } - // 使用现有的消息处理流程 -// [self.messageContainerView handleNIMCustomMessage:message]; -// [self.animationView handleNIMCustomMessage:message]; -// [self handleNimCustomTypeMessage:message]; -// [self onRecvMessages:@[message]]; - - switch (message.messageType) { case NIMMessageTypeNotification: [self handleNIMNotificationTypeMessage:message]; @@ -3416,7 +3573,7 @@ XPCandyTreeInsufficientBalanceViewDelegate> NSLog(@"🎮 卡顿检测模拟已触发,计数将增加"); } -/// 调试方法:在所有 StageView 上绘制符合条件的相邻麦位中点矩形 +/// 在 StageView 上绘制符合条件的相邻麦位中点矩形 - (void)drawSocialStageMidpointRects { if (!self.stageView) { NSLog(@"🔧 当前没有 stageView,跳过中点矩形绘制"); @@ -3485,13 +3642,13 @@ XPCandyTreeInsufficientBalanceViewDelegate> CGRect rect = [self.stageView rectForMidpointBetweenMicAtIndex:firstIndex andIndex:secondIndex]; if (!CGRectIsEmpty(rect)) { - UIView *debugView = [[UIView alloc] initWithFrame:rect]; - debugView.backgroundColor = [[UIColor blueColor] colorWithAlphaComponent:0.3]; - debugView.layer.borderColor = [UIColor blueColor].CGColor; - debugView.layer.borderWidth = 2.0; - debugView.layer.cornerRadius = 8.0; - debugView.tag = 56002; - debugView.userInteractionEnabled = NO; + UIView *micRelationshipView = [[UIView alloc] initWithFrame:rect]; + micRelationshipView.backgroundColor = [[UIColor blueColor] colorWithAlphaComponent:0.3]; + micRelationshipView.layer.borderColor = [UIColor blueColor].CGColor; + micRelationshipView.layer.borderWidth = 2.0; + micRelationshipView.layer.cornerRadius = 8.0; + micRelationshipView.tag = 56002; + micRelationshipView.userInteractionEnabled = NO; // 添加标签显示麦位对 UILabel *label = [[UILabel alloc] init]; @@ -3499,23 +3656,22 @@ XPCandyTreeInsufficientBalanceViewDelegate> label.textColor = [UIColor whiteColor]; label.font = [UIFont boldSystemFontOfSize:12]; label.textAlignment = NSTextAlignmentCenter; - label.frame = debugView.bounds; - [debugView addSubview:label]; + label.frame = micRelationshipView.bounds; + [micRelationshipView addSubview:label]; - // 根据不同的 StageView 类型添加到相应的容器 - if ([self.stageView isKindOfClass:[LittleGameScrollStageView class]]) { - [(LittleGameScrollStageView *)self.stageView addMidpointRect:debugView]; - } else if ([self.stageView isKindOfClass:[LittleGameStageView class]]) { - [(LittleGameStageView *)self.stageView addMidpointRect:debugView]; - } else if ([self.stageView respondsToSelector:@selector(midpointRectManager)]) { - // 使用新的管理器添加中点矩形 + // 统一逻辑:优先通过 midpointRectManager 管理器添加与播放SVGA;无管理器时回退为直接添加 + if ([self.stageView respondsToSelector:@selector(midpointRectManager)]) { id manager = [self.stageView valueForKey:@"midpointRectManager"]; - if ([manager respondsToSelector:@selector(addMidpointRectAtFrame:micPairText:autoPlaySVGA:)]) { + if ([manager respondsToSelector:@selector(addRelationshipAtFrame:micPairText:leftUid:rightUid:cpList:)]) { NSString *micPairText = [NSString stringWithFormat:@"%ld-%ld", (long)firstIndex, (long)secondIndex]; - [manager addMidpointRectAtFrame:rect micPairText:micPairText autoPlaySVGA:YES]; + UIView *leftView = [self.stageView findMicroViewByIndex:firstIndex]; + UIView *rightView = [self.stageView findMicroViewByIndex:secondIndex]; + NSInteger leftUid = [[leftView getUser] uid]; + NSInteger rightUid = [[rightView getUser] uid]; + [manager addRelationshipAtFrame:rect micPairText:micPairText leftUid:leftUid rightUid:rightUid cpList:self.currentCpList ?: @[]]; } } else { - [self.stageView addSubview:debugView]; + [self.stageView addSubview:micRelationshipView]; } NSLog(@"🔧 绘制中点矩形: %ld-%ld, rect: %@", (long)firstIndex, (long)secondIndex, NSStringFromCGRect(rect)); } else { @@ -3526,4 +3682,387 @@ XPCandyTreeInsufficientBalanceViewDelegate> NSLog(@"🔧 %@ 中点矩形绘制完成", stageViewClass); } +/// 进房成功后调用CP相关API +- (void)callMicCpListByRoomUidAfterRoomEntered { + NSLog(@"🔧 进房成功:开始调用 micCpListByRoomUid API"); + + if (!self.roomInfo) { + NSLog(@"⚠️ 进房成功:roomInfo为空,跳过CP API调用"); + return; + } + + NSString *roomUid = [NSString stringWithFormat:@"%ld", (long)self.roomInfo.uid]; + NSLog(@"🔧 进房成功:房间UID: %@, 房间类型: %ld", roomUid, (long)self.roomInfo.type); + + // 调用 micCpListByRoomUid 方法 + if ([self.presenter respondsToSelector:@selector(micCpListByRoomUid:)]) { + NSLog(@"🔧 进房成功:调用 micCpListByRoomUid: %@", roomUid); + [self.presenter micCpListByRoomUid:roomUid]; + } else { + NSLog(@"⚠️ 进房成功:presenter不支持micCpListByRoomUid方法"); + } + + NSLog(@"🔧 进房成功:micCpListByRoomUid API调用完成"); +} + +/// 获取当前用户及其左右相邻用户的uid列表(最多3个) +- (NSArray *)getAllMicUserUidsWithQueue:(NSMutableDictionary *)queue { + NSMutableArray *micUserUids = [NSMutableArray array]; + + if (!self.roomInfo) { + NSLog(@"⚠️ 获取mic用户列表:roomInfo为空"); + return micUserUids; + } + + // 获取当前用户的麦位位置 + NSInteger currentUserMicPosition = self.currentUserMicPosition; + if (currentUserMicPosition == -1) { + NSLog(@"🔧 获取mic用户列表:当前用户不在麦上,返回空数据"); + return micUserUids; + } + + // 获取最大麦位数 + NSInteger maxMicCount = 0; + switch (self.roomInfo.type) { + case RoomType_Game: + maxMicCount = 9; + break; + case RoomType_10Mic: + maxMicCount = 10; + break; + case RoomType_15Mic: + maxMicCount = 15; + break; + case RoomType_19Mic: + maxMicCount = 19; + break; + case RoomType_20Mic: + maxMicCount = 20; + break; + default: + maxMicCount = 9; // 默认9麦 + break; + } + + // 获取当前用户及其左右相邻用户的UID + NSInteger leftPosition = currentUserMicPosition - 1; + NSInteger rightPosition = currentUserMicPosition + 1; + + // 获取左边用户 + if (leftPosition >= 0) { + NSString *leftPositionKey = [NSString stringWithFormat:@"%ld", (long)leftPosition]; + MicroQueueModel *leftMicModel = queue[leftPositionKey]; + if (leftMicModel && leftMicModel.userInfo && leftMicModel.userInfo.uid > 0) { + [micUserUids addObject:[NSString stringWithFormat:@"%ld", (long)leftMicModel.userInfo.uid]]; + } + } + + // 获取当前用户 + NSString *currentPositionKey = [NSString stringWithFormat:@"%ld", (long)currentUserMicPosition]; + MicroQueueModel *currentMicModel = queue[currentPositionKey]; + if (currentMicModel && currentMicModel.userInfo && currentMicModel.userInfo.uid > 0) { + [micUserUids addObject:[NSString stringWithFormat:@"%ld", (long)currentMicModel.userInfo.uid]]; + } + + // 获取右边用户 + if (rightPosition < maxMicCount) { + NSString *rightPositionKey = [NSString stringWithFormat:@"%ld", (long)rightPosition]; + MicroQueueModel *rightMicModel = queue[rightPositionKey]; + if (rightMicModel && rightMicModel.userInfo && rightMicModel.userInfo.uid > 0) { + [micUserUids addObject:[NSString stringWithFormat:@"%ld", (long)rightMicModel.userInfo.uid]]; + } + } + + // 如果只有当前用户一个人,返回空数据 + if (micUserUids.count == 1) { + NSLog(@"🔧 获取mic用户列表:只有当前用户一个人,返回空数据"); + return [NSArray array]; + } + + NSLog(@"🔧 获取mic用户列表:当前用户麦位 %ld,找到 %lu 个相关用户: %@", + (long)currentUserMicPosition, (unsigned long)micUserUids.count, micUserUids); + return micUserUids; +} + +/// 检测并处理下麦事件 +- (void)handleDownMicEventIfNeeded:(NSMutableDictionary *)queue { + if (!self.stageView || ![self.stageView respondsToSelector:@selector(midpointRectManager)]) { + NSLog(@"🔧 处理下麦事件:stageView 不支持 midpointRectManager,跳过处理"); + return; + } + + // 获取当前舞台类型的麦位总数 + NSInteger micCount = 0; + if (self.roomInfo) { + switch (self.roomInfo.type) { + case RoomType_Game: micCount = 9; break; + case RoomType_10Mic: micCount = 10; break; + case RoomType_15Mic: micCount = 15; break; + case RoomType_19Mic: micCount = 19; break; + case RoomType_20Mic: micCount = 20; break; + default: micCount = 9; break; + } + } + + // 使用 MicMidpointRectManager 检测麦位变化 +// MicMidpointRectManager *midpointRectManager = [self.stageView midpointRectManager]; + MicMidpointRectManager *midpointRectManager = (MicMidpointRectManager *)[self.stageView performSelector:@selector(midpointRectManager)]; + if (midpointRectManager) { + NSDictionary *> *micChanges = + [midpointRectManager diffMicChangeWithStageView:self.stageView micCount:micCount]; + + NSArray *removedUids = micChanges[@"removed"]; + if (removedUids && removedUids.count > 0) { + NSLog(@"🔧 检测到下麦用户:%@", removedUids); + + // 调用 MicMidpointRectManager 处理下麦事件 + [midpointRectManager handleDownMicEvent:removedUids stageView:self.stageView micCount:micCount]; + } + } +} + +/// mic位变动时调用CP相关API +- (void)callMicCpListByUidListOnMicChangeWithQueue:(NSMutableDictionary *)queue { + NSLog(@"🔧 mic位变动:开始调用 micCpListByUidList API"); + + NSArray *micUserUids = [self getAllMicUserUidsWithQueue:queue]; + + if (micUserUids.count == 0) { + NSLog(@"🔧 mic位变动:没有相关用户在mic上,跳过API调用"); + return; + } + + // 调用 micCpListByUidList 方法 + if ([micUserUids containsObject:[AccountInfoStorage instance].getUid]) { + NSLog(@"🔧 mic位变动,且包含了当前用户:调用 micCpListByUidList: %@", micUserUids); + [self.presenter micCpListByUidList:micUserUids]; + } + + NSLog(@"🔧 mic位变动:micCpListByUidList API调用完成"); +} + +/// 初始化当前用户的麦位状态 +- (void)initializeCurrentUserMicStatus { + NSLog(@"🔧 初始化当前用户麦位状态"); + + // 重置状态 + self.currentUserMicStatusChanged = NO; + self.currentUserWasOnMic = NO; + self.currentUserMicPosition = -1; + self.hasCompletedRoomInitialization = NO; // 标记为未完成初始化 + + // 获取当前麦位状态 + NSMutableDictionary *currentQueue = [self.stageView getMicroQueue]; + if (currentQueue && currentQueue.count > 0) { + NSDictionary *currentStatus = [self getCurrentUserMicStatus:currentQueue]; + BOOL isOnMic = [currentStatus[@"isOnMic"] boolValue]; + NSInteger micPosition = [currentStatus[@"micPosition"] integerValue]; + + self.currentUserWasOnMic = isOnMic; + self.currentUserMicPosition = micPosition; + + NSLog(@"🔧 初始化完成 - 当前用户是否在麦上: %@, 麦位: %ld", + isOnMic ? @"是" : @"否", (long)micPosition); + } else { + NSLog(@"🔧 初始化完成 - 当前没有麦位数据"); + } + + // 延迟设置初始化完成标志,确保进房时的麦位更新不会触发API调用 + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ + self.hasCompletedRoomInitialization = YES; + NSLog(@"🔧 进房初始化完成,后续麦位变动将触发 micCpListByUidList 调用"); + }); +} + +/// 获取当前用户的麦位状态 +- (NSDictionary *)getCurrentUserMicStatus:(NSMutableDictionary *)queue { + NSInteger currentUid = [AccountInfoStorage instance].getUid.integerValue; + BOOL isOnMic = NO; + NSInteger micPosition = -1; + + for (NSString *position in queue.allKeys) { + MicroQueueModel *micModel = queue[position]; + if (micModel.userInfo.uid > 0 && micModel.userInfo.uid == currentUid) { + isOnMic = YES; + micPosition = position.integerValue; + break; + } + } + + return @{ + @"isOnMic": @(isOnMic), + @"micPosition": @(micPosition) + }; +} + +/// 检查当前用户麦位状态是否发生变化(上麦、下麦、换位) +- (BOOL)checkCurrentUserMicStatusChanged:(NSMutableDictionary *)queue { + NSDictionary *currentStatus = [self getCurrentUserMicStatus:queue]; + BOOL currentUserOnMic = [currentStatus[@"isOnMic"] boolValue]; + NSInteger currentMicPosition = [currentStatus[@"micPosition"] integerValue]; + + // 情况1:之前不在麦上,现在在麦上(上麦) + if (currentUserOnMic && !self.currentUserWasOnMic) { + NSLog(@"🔧 检测到当前用户刚刚上麦,麦位: %ld", (long)currentMicPosition); + return YES; + } + + // 情况2:之前在麦上,现在不在麦上(下麦) + if (!currentUserOnMic && self.currentUserWasOnMic) { + NSLog(@"🔧 检测到当前用户刚刚下麦,之前麦位: %ld", (long)self.currentUserMicPosition); + return YES; + } + + // 情况3:之前在麦上,现在也在麦上,但位置不同(换位) + if (currentUserOnMic && self.currentUserWasOnMic && + currentMicPosition != self.currentUserMicPosition) { + NSLog(@"🔧 检测到当前用户换麦位,从 %ld 换到 %ld", + (long)self.currentUserMicPosition, (long)currentMicPosition); + return YES; + } + + return NO; +} + +/// 更新当前用户的麦位状态 +- (void)updateCurrentUserMicStatus:(NSMutableDictionary *)queue { + NSDictionary *currentStatus = [self getCurrentUserMicStatus:queue]; + BOOL currentUserOnMic = [currentStatus[@"isOnMic"] boolValue]; + NSInteger micPosition = [currentStatus[@"micPosition"] integerValue]; + + // 检查麦位状态是否发生变化(上麦、下麦、换位) + if ([self checkCurrentUserMicStatusChanged:queue]) { + self.currentUserMicStatusChanged = YES; + } + + // 更新状态 + self.currentUserWasOnMic = currentUserOnMic; + self.currentUserMicPosition = micPosition; +} + +/// 发送麦位关系 NIM message +- (void)sendMicRelationshipNIMessage:(NSArray *)cpList { + NSLog(@"🔧 发送麦位关系 NIM message,CP数据条数: %ld,当前用户麦位: %ld", + (long)cpList.count, (long)self.currentUserMicPosition); + + if (!self.roomInfo) { + NSLog(@"⚠️ 发送麦位关系 NIM message:roomInfo为空,跳过发送"); + return; + } + + // 将 CP 数据转换为 JSON string + NSMutableArray *cpDataArray = [NSMutableArray array]; + for (MicCpInfoModel *cpInfo in cpList) { + NSDictionary *cpDict = @{ + @"uid": @(cpInfo.uid), + @"loverUid": @(cpInfo.loverUid), + @"cpLevel": @(cpInfo.cpLevel) + }; + [cpDataArray addObject:cpDict]; + } + + NSDictionary *finalData = @{@"first": @(MicRelationship_Type), + @"second":@(MicRelationship_CP), + @"data":cpDataArray.copy}; + + NSError *error = nil; + NSData *jsonData = [NSJSONSerialization dataWithJSONObject:finalData options:0 error:&error]; + if (error) { + NSLog(@"❌ 发送麦位关系 NIM message:JSON序列化失败 - %@", error.localizedDescription); + return; + } + + NSString *jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding]; + NSLog(@"🔧 发送麦位关系 NIM message:JSON数据 - %@", jsonString); + + // 创建 AttachmentModel + AttachmentModel *attachment = [[AttachmentModel alloc] init]; + attachment.first = MicRelationship_Type; // 1001 + attachment.second = MicRelationship_CP; // 10011 + attachment.data = jsonString; + + // 创建 NIM message + NIMMessage *message = [[NIMMessage alloc] init]; + NIMCustomObject *object = [[NIMCustomObject alloc] init]; + object.attachment = attachment; + message.messageObject = object; + + // 设置用户信息到 remoteExt + UserInfoModel *userInfo = [self getUserInfo]; + XPMessageRemoteExtModel *extModel = [[XPMessageRemoteExtModel alloc] init]; + extModel.androidBubbleUrl = userInfo.androidBubbleUrl; + extModel.iosBubbleUrl = userInfo.iosBubbleUrl; + extModel.fromSayHelloChannel = userInfo.fromSayHelloChannel; + extModel.platformRole = userInfo.platformRole; + NSMutableDictionary *remoteExt = [NSMutableDictionary dictionaryWithObject:extModel.model2dictionary forKey:[NSString stringWithFormat:@"%ld", userInfo.uid]]; + message.remoteExt = remoteExt; + + // 构造会话 + NSString *sessionID = [NSString stringWithFormat:@"%ld", self.roomInfo.roomId]; + NIMSession *session = [NIMSession session:sessionID type:NIMSessionTypeChatroom]; + + // 发送消息 + [[NIMSDK sharedSDK].chatManager sendMessage:message toSession:session completion:^(NSError * _Nullable error) { + if (error) { + NSLog(@"❌ 发送麦位关系 NIM message 失败 - %@", error.localizedDescription); + } else { + NSLog(@"✅ 发送麦位关系 NIM message 成功"); + } + }]; +} + +/// 更新 MicMidpointRectManager 的缓存 +- (void)updateMicMidpointRectManagerCache:(NSArray *)cpList { + if (!self.stageView) { + NSLog(@"�� 更新缓存:stageView 为空,跳过缓存更新"); + return; + } + + MicMidpointRectManager *midpointRectManager = (MicMidpointRectManager *)[self.stageView performSelector:@selector(midpointRectManager)]; + if (midpointRectManager) { + // 获取当前用户及其相邻用户的UID列表 + NSMutableDictionary *currentQueue = [self.stageView getMicroQueue]; + NSArray *micUserUids = [self getAllMicUserUidsWithQueue:currentQueue]; + + // 转换为 NSNumber 数组 + NSMutableArray *uidNumbers = [NSMutableArray array]; + for (NSString *uidString in micUserUids) { + [uidNumbers addObject:@(uidString.integerValue)]; + } + + // 更新缓存:先移除相关用户的旧数据,再合并新数据 + [midpointRectManager mergeCpListCache:cpList replaceForUids:uidNumbers]; + + NSLog(@"�� 更新缓存完成:CP数据条数 %lu,相关用户 %@", + (unsigned long)cpList.count, uidNumbers); + } +} + +/// 更新 MicMidpointRectManager 的麦位快照 +- (void)updateMicMidpointRectManagerSnapshot { + if (!self.stageView) { + NSLog(@"�� 更新快照:stageView 为空,跳过快照更新"); + return; + } + + // 获取当前舞台类型的麦位总数 + NSInteger micCount = 0; + if (self.roomInfo) { + switch (self.roomInfo.type) { + case RoomType_Game: micCount = 9; break; + case RoomType_10Mic: micCount = 10; break; + case RoomType_15Mic: micCount = 15; break; + case RoomType_19Mic: micCount = 19; break; + case RoomType_20Mic: micCount = 20; break; + default: micCount = 9; break; + } + } + + MicMidpointRectManager *midpointRectManager = (MicMidpointRectManager *)[self.stageView performSelector:@selector(midpointRectManager)]; + if (midpointRectManager) { + [midpointRectManager rebuildMicSnapshotWithStageView:self.stageView micCount:micCount]; + NSLog(@"🔧 更新麦位快照完成:麦位总数 %ld", (long)micCount); + } +} + @end