Commit e624f859 authored by hex's avatar hex

fix mycard crash

parent 0af8596e
Pipeline #38794 failed
......@@ -129,8 +129,8 @@ public class MonoCardInDeckManager : MonoBehaviour
var rigidbody = GetComponent<Rigidbody>();
if (rigidbody != null) rigidbody.Sleep();
transform.DOMove(position, 0.2f).OnComplete(physicalON);
transform.DORotate(rotation, 0.15f);
transform.DOMove(position, 0.1f).OnComplete(physicalON);
transform.DORotate(rotation, 0.1f);
physicalOFF();
}
......
......@@ -5,13 +5,33 @@ public class descKeeper : MonoBehaviour
{
public UITexture card;
public UITexture back;
// Use this for initialization
void Start() { }
private CardDescription cardDesc = null;
void Start()
{
// Try to get the reference once at the beginning.
if (Program.I() != null)
{
cardDesc = Program.I().cardDescription;
}
}
// Update is called once per frame
void Update()
{
// Use the cached reference. If it's still null, it means it wasn't ready in Start.
// It might become available later, so we re-check.
if (cardDesc == null)
{
if (Program.I() != null)
{
cardDesc = Program.I().cardDescription;
}
// If it's still null after re-checking, exit for this frame.
if (cardDesc == null) return;
}
// Also ensure card and back are assigned in the inspector
if (card == null || back == null) return;
if (back.width < card.width)
{
back.width = card.width + 2;
......@@ -25,7 +45,7 @@ public class descKeeper : MonoBehaviour
leftTop.x + card.width / 2,
leftTop.y - card.height / 2
);
Program.I().cardDescription.width = back.width - 2;
Program.I().cardDescription.cHeight = card.height;
cardDesc.width = back.width - 2;
cardDesc.cHeight = card.height;
}
}
......@@ -540,9 +540,9 @@ public static class TcpHelper
startI = i;
}
}
catch (System.Exception e)
catch (Exception e)
{
UnityEngine.Debug.Log(e);
Debug.Log(e);
}
}
if (write)
......@@ -568,9 +568,9 @@ public static class TcpHelper
}
packagesInRecord.Clear();
}
catch (System.Exception e)
catch (Exception e)
{
UnityEngine.Debug.Log(e);
Debug.Log(e);
}
}
......
using UnityEngine;
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Threading;
......@@ -11,7 +12,10 @@ public class MyCard : WindowServantSP
//const string mycardTiramisuAddress = "tiramisu.mycard.moe";
//const string mycardTiramisuAthleticPort = "8911";
//const string mycardTiramisuEntertainPort = "7911";
Thread requestThread = null;
// 不再需要显式的 Thread 对象
// Thread requestThread = null;
private Coroutine requestCoroutine = null; // 用 Coroutine 对象来跟踪
MyCardHelper mycardHelper;
UIInput inputUsername;
UIInput inputPsw;
......@@ -31,108 +35,197 @@ public class MyCard : WindowServantSP
SetActiveFalse();
}
void saveUser() {
void saveUser()
{
Config.Set("mycard_username", inputUsername.value);
Config.Set("mycard_password", inputPsw.value);
Program.I().selectServer.name = inputUsername.value;
}
void loadUser() {
void loadUser()
{
inputUsername.value = Config.Get("mycard_username", "MyCard");
inputPsw.value = Config.Get("mycard_password", "");
}
public void terminateThread()
{
if (!isRequesting && requestThread == null)
// public void terminateThread()
// {
// if (!isRequesting && requestThread == null)
// {
// return;
// }
// requestThread.Abort();
// requestThread = null;
// }
public void terminateRequest()
{
if (requestCoroutine != null)
{
return;
Program.I().StopCoroutine(requestCoroutine);
requestCoroutine = null;
isRequesting = false;
Program.PrintToChat(InterString.Get("匹配已中断。"));
}
requestThread.Abort();
requestThread = null;
}
// void onClickExit()
// {
// Program.I().shiftToServant(Program.I().menu);
// if (TcpHelper.tcpClient != null)
// {
// if (isRequesting) {
// terminateThread();
// }
// if (TcpHelper.tcpClient.Connected)
// {
// TcpHelper.tcpClient.Close();
// }
// }
// }
void onClickExit()
{
Program.I().shiftToServant(Program.I().menu);
if (TcpHelper.tcpClient != null)
if (isRequesting)
{
terminateRequest();
}
if (TcpHelper.tcpClient != null && TcpHelper.tcpClient.Connected)
{
if (isRequesting) {
terminateThread();
}
if (TcpHelper.tcpClient.Connected)
{
TcpHelper.tcpClient.Close();
}
TcpHelper.tcpClient.Close();
}
}
void onClickDatabase() {
void onClickDatabase()
{
Application.OpenURL("https://mycard.moe/ygopro/arena/");
}
void onClickCommunity() {
void onClickCommunity()
{
Application.OpenURL("https://ygobbs.com/");
}
void matchThread(string username, string password, string matchType) {
try {
Program.PrintToChat(InterString.Get("正在登录至 MyCard。"));
string failReason = "";
bool res = mycardHelper.login(username, password, out failReason);
if (!res) {
Program.PrintToChat(InterString.Get("MyCard 登录失败。原因: ") + failReason);
isRequesting = false;
return;
}
Program.PrintToChat(InterString.Get("MyCard 登录成功,用户名: ") + mycardHelper.username);
Program.PrintToChat(InterString.Get("正在请求匹配。匹配类型: ") + matchType);
MatchResultObject matchResultObject = mycardHelper.requestMatch(matchType, out failReason);
if (matchResultObject == null) {
Program.PrintToChat(InterString.Get("匹配请求失败。原因: ") + failReason);
isRequesting = false;
return;
}
Program.PrintToChat(InterString.Get("匹配成功。正在进入房间。"));
this.isMatching = true;
// void matchThread(string username, string password, string matchType) {
// try {
// Program.PrintToChat(InterString.Get("正在登录至 MyCard。"));
// string failReason = "";
// bool res = mycardHelper.login(username, password, out failReason);
// if (!res) {
// Program.PrintToChat(InterString.Get("MyCard 登录失败。原因: ") + failReason);
// isRequesting = false;
// return;
// }
// Program.PrintToChat(InterString.Get("MyCard 登录成功,用户名: ") + mycardHelper.username);
// Program.PrintToChat(InterString.Get("正在请求匹配。匹配类型: ") + matchType);
// MatchResultObject matchResultObject = mycardHelper.requestMatch(matchType, out failReason);
// if (matchResultObject == null) {
// Program.PrintToChat(InterString.Get("匹配请求失败。原因: ") + failReason);
// isRequesting = false;
// return;
// }
// Program.PrintToChat(InterString.Get("匹配成功。正在进入房间。"));
// this.isMatching = true;
(new Thread(() => { TcpHelper.join(matchResultObject.address, mycardHelper.username, matchResultObject.port.ToString(), matchResultObject.password, "0x" + String.Format("{0:X}", Config.ClientVersion)); })).Start();
// (new Thread(() => { TcpHelper.join(matchResultObject.address, mycardHelper.username, matchResultObject.port.ToString(), matchResultObject.password, "0x" + String.Format("{0:X}", Config.ClientVersion)); })).Start();
// isRequesting = false;
// } catch (Exception e) {
// if (e.GetType() != typeof(ThreadAbortException)) {
// Program.PrintToChat(InterString.Get("未知错误: ") + e.Message);
// } else {
// Program.PrintToChat(InterString.Get("匹配已中断。"));
// }
// isRequesting = false;
// }
// }
// void startMatch(string matchType) {
// string username = inputUsername.value;
// string password = inputPsw.value;
// if (username == "" || password == "")
// {
// RMSshow_onlyYes("", InterString.Get("用户名或密码为空。"), null);
// return;
// }
// if (isRequesting)
// {
// terminateThread();
// }
// saveUser();
// isRequesting = true;
// Program.PrintToChat(InterString.Get("已开始匹配。"));
// requestThread = new Thread(() =>
// {
// matchThread(username, password, matchType);
// });
// requestThread.Start();
// }
// void onClickJoinAthletic() {
// startMatch("athletic");
// }
// void onClickJoinEntertain() {
// startMatch("entertain");
// }
IEnumerator matchCoroutine(string username, string password, string matchType) {
isRequesting = true;
Program.PrintToChat(InterString.Get("正在登录至 MyCard。"));
bool loginSuccess = false;
string failReason = "";
// 启动并等待登录协程完成
yield return Program.I().StartCoroutine(mycardHelper.login(username, password, (success, reason) => {
loginSuccess = success;
failReason = reason;
}));
if (!loginSuccess) {
Program.PrintToChat(InterString.Get("MyCard 登录失败。原因: ") + failReason);
isRequesting = false;
} catch (Exception e) {
if (e.GetType() != typeof(ThreadAbortException)) {
Program.PrintToChat(InterString.Get("未知错误: ") + e.Message);
} else {
Program.PrintToChat(InterString.Get("匹配已中断。"));
}
yield break; // 结束协程
}
Program.PrintToChat(InterString.Get("MyCard 登录成功,用户名: ") + mycardHelper.username);
Program.PrintToChat(InterString.Get("正在请求匹配。匹配类型: ") + matchType);
MatchResultObject matchResultObject = null;
// 启动并等待匹配协程完成
yield return Program.I().StartCoroutine(mycardHelper.requestMatch(matchType, (result, reason) => {
matchResultObject = result;
failReason = reason;
}));
if (matchResultObject == null) {
Program.PrintToChat(InterString.Get("匹配请求失败。原因: ") + failReason);
isRequesting = false;
yield break;
}
Program.PrintToChat(InterString.Get("匹配成功。正在进入房间。"));
this.isMatching = true;
// TcpHelper.join 内部自己创建并管理线程,这里直接调用是安全的
// 只要 TcpHelper.join 本身及其后续操作不调用 Unity API
(new Thread(() => { TcpHelper.join(matchResultObject.address, mycardHelper.username, matchResultObject.port.ToString(), matchResultObject.password, "0x" + String.Format("{0:X}", Config.ClientVersion)); })).Start();
isRequesting = false;
requestCoroutine = null;
}
void startMatch(string matchType) {
string username = inputUsername.value;
string password = inputPsw.value;
if (username == "" || password == "")
{
if (string.IsNullOrEmpty(username) || string.IsNullOrEmpty(password))
{
RMSshow_onlyYes("", InterString.Get("用户名或密码为空。"), null);
return;
}
if (isRequesting)
{
terminateThread();
terminateRequest();
}
saveUser();
isRequesting = true;
Program.PrintToChat(InterString.Get("已开始匹配。"));
requestThread = new Thread(() =>
{
matchThread(username, password, matchType);
});
requestThread.Start();
}
// 启动协程,而不是线程
requestCoroutine = Program.I().StartCoroutine(matchCoroutine(username, password, matchType));
}
void onClickJoinAthletic() {
startMatch("athletic");
}
void onClickJoinEntertain() {
startMatch("entertain");
}
......
......@@ -15,6 +15,7 @@ Please send emails to pokeboyexn@gmail.com for further information.
using UnityEngine;
using System;
using System.Text;
using System.Collections;
using System.Collections.Generic;
using System.IO;
......@@ -58,67 +59,129 @@ public class MatchResultObject {
public class MyCardHelper {
public string username = null;
int userid = -1;
public bool login(string name, string password, out string failReason) {
try {
LoginRequest data = new LoginRequest();
data.account = name;
data.password = password;
string data_str = JsonUtility.ToJson(data);
Dictionary<String, String> header_list = new Dictionary<String, String>();
header_list.Add("Content-Type", "application/json");
byte[] data_bytes = Encoding.UTF8.GetBytes(data_str);
WWW www = new WWW("https://sapi.moecube.com:444/accounts/signin", data_bytes, header_list);
while (!www.isDone) {
if (Application.internetReachability == NetworkReachability.NotReachable || !string.IsNullOrEmpty(www.error))
{
failReason = www.error;
return false;
}
}
string result = www.text;
LoginObject result_object = JsonUtility.FromJson<LoginObject>(result);
username = result_object.user.username;
userid = result_object.user.id;
} catch (Exception e) {
failReason = e.Message;
return false;
// public bool login(string name, string password, out string failReason) {
// try {
// LoginRequest data = new LoginRequest();
// data.account = name;
// data.password = password;
// string data_str = JsonUtility.ToJson(data);
// Dictionary<String, String> header_list = new Dictionary<String, String>();
// header_list.Add("Content-Type", "application/json");
// byte[] data_bytes = Encoding.UTF8.GetBytes(data_str);
// WWW www = new WWW("https://sapi.moecube.com:444/accounts/signin", data_bytes, header_list);
// while (!www.isDone) {
// if (Application.internetReachability == NetworkReachability.NotReachable || !string.IsNullOrEmpty(www.error))
// {
// failReason = www.error;
// return false;
// }
// }
// string result = www.text;
// LoginObject result_object = JsonUtility.FromJson<LoginObject>(result);
// username = result_object.user.username;
// userid = result_object.user.id;
// } catch (Exception e) {
// failReason = e.Message;
// return false;
// }
// failReason = null;
// return true;
// }
// public MatchResultObject requestMatch(string matchType, out string failReason) {
// MatchResultObject matchResultObject;
// if (username == null || userid < 0) {
// failReason = "Not logged in";
// return null;
// }
// try {
// string auth_str = "Basic " + Convert.ToBase64String(Encoding.UTF8.GetBytes(username + ":" + userid));
// Dictionary<String, String> header_list = new Dictionary<String, String>();
// header_list.Add("Authorization", auth_str);
// header_list.Add("Content-Type", "application/x-www-form-urlencoded");
// byte[] meta = new byte[1];
// WWW www = new WWW("https://sapi.moecube.com:444/ygopro/match?locale=zh-CN&arena=" + matchType, meta, header_list);
// while (!www.isDone) {
// if (Application.internetReachability == NetworkReachability.NotReachable || !string.IsNullOrEmpty(www.error))
// {
// failReason = www.error;
// return null;
// }
// }
// string result = www.text;
// matchResultObject = JsonUtility.FromJson<MatchResultObject>(result);
// } catch (Exception e) {
// failReason = e.Message;
// return null;
// }
// failReason = null;
// return matchResultObject;
// }
// 改为 IEnumerator 协程
public IEnumerator login(string name, string password, Action<bool, string> onComplete) {
LoginRequest data = new LoginRequest();
data.account = name;
data.password = password;
string data_str = JsonUtility.ToJson(data);
Dictionary<String, String> header_list = new Dictionary<String, String>();
header_list.Add("Content-Type", "application/json");
byte[] data_bytes = Encoding.UTF8.GetBytes(data_str);
WWW www = new WWW("https://sapi.moecube.com:444/accounts/signin", data_bytes, header_list);
// 使用 yield return 等待 WWW 完成,而不是 while 循环
yield return www;
if (!string.IsNullOrEmpty(www.error))
{
onComplete(false, www.error);
yield break; // 提前退出协程
}
failReason = null;
return true;
try {
string result = www.text;
LoginObject result_object = JsonUtility.FromJson<LoginObject>(result);
// 检查返回结果是否有效
if (result_object == null || result_object.user == null) {
onComplete(false, "Login failed: Invalid response from server. " + result);
yield break;
}
username = result_object.user.username;
userid = result_object.user.id;
onComplete(true, null);
} catch (Exception e) {
onComplete(false, e.Message);
}
}
public MatchResultObject requestMatch(string matchType, out string failReason) {
MatchResultObject matchResultObject;
// 同样改为 IEnumerator 协程
public IEnumerator requestMatch(string matchType, Action<MatchResultObject, string> onComplete) {
if (username == null || userid < 0) {
failReason = "Not logged in";
return null;
onComplete(null, "Not logged in");
yield break;
}
try {
string auth_str = "Basic " + Convert.ToBase64String(Encoding.UTF8.GetBytes(username + ":" + userid));
Dictionary<String, String> header_list = new Dictionary<String, String>();
header_list.Add("Authorization", auth_str);
header_list.Add("Content-Type", "application/x-www-form-urlencoded");
byte[] meta = new byte[1];
WWW www = new WWW("https://sapi.moecube.com:444/ygopro/match?locale=zh-CN&arena=" + matchType, meta, header_list);
while (!www.isDone) {
if (Application.internetReachability == NetworkReachability.NotReachable || !string.IsNullOrEmpty(www.error))
{
failReason = www.error;
return null;
}
}
string result = www.text;
matchResultObject = JsonUtility.FromJson<MatchResultObject>(result);
} catch (Exception e) {
failReason = e.Message;
return null;
}
failReason = null;
return matchResultObject;
string auth_str = "Basic " + Convert.ToBase64String(Encoding.UTF8.GetBytes(username + ":" + userid));
Dictionary<String, String> header_list = new Dictionary<String, String>();
header_list.Add("Authorization", auth_str);
header_list.Add("Content-Type", "application/x-www-form-urlencoded");
byte[] meta = new byte[1];
WWW www = new WWW("https://sapi.moecube.com:444/ygopro/match?locale=zh-CN&arena=" + matchType, meta, header_list);
yield return www;
if (!string.IsNullOrEmpty(www.error))
{
onComplete(null, www.error);
yield break;
}
try {
string result = www.text;
MatchResultObject matchResultObject = JsonUtility.FromJson<MatchResultObject>(result);
onComplete(matchResultObject, null);
} catch (Exception e) {
onComplete(null, e.Message);
}
}
public static string DownloadFace(string name) {
try {
public static string DownloadFace(string name)
{
try
{
string face = "textures/face/" + name + ".png";
HttpDldFile df = new HttpDldFile();
df.Download("http://api.moestart.com/avatar/avatar/" + name + "/100/koishipro2.png", face);
......@@ -130,7 +193,9 @@ public class MyCardHelper {
return null;
}
return "Not downloaded";
} catch (Exception e) {
}
catch (Exception e)
{
return e.Message;
}
}
......
using System;
using UnityEngine;
using YGOSharp.OCGWrapper.Enums;
using YGOSharp.OCGWrapper.Enums;
public class gameHiddenButton : OCGobject
{
public CardLocation location;
......@@ -291,8 +291,6 @@ public class gameHiddenButton : OCGobject
qidian
+
((float)index / (float)(gezi - 1)) * (zhongdian - qidian);
//iTween.MoveTo(Program.I().ocgcore.cards[i].gameObject, Camera.main.ScreenToWorldPoint(screen_vector_to_move), 0.5f);
//iTween.RotateTo(Program.I().ocgcore.cards[i].gameObject, new Vector3(-30, 0, 0), 0.1f);
Program.I().ocgcore.cards[i].TweenTo(Camera.main.ScreenToWorldPoint(screen_vector_to_move), new Vector3(-30, 0, 0),true);
}
}
......
......@@ -33,7 +33,7 @@ public class HttpDldFile
}
if (Path.GetExtension(filename).Contains("jpg"))
{
client.Timeout = 3500;
client.Timeout = 6500;
}
if (Path.GetExtension(filename).Contains("cdb"))
{
......
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Text;
......@@ -165,26 +166,66 @@ public class Room : WindowServantSP
RoomPlayer player = new RoomPlayer();
player.name = name;
player.prep = false;
if (Program.I().mycard.isMatching && name != "********") // athletic match name mask
{
(
new Thread(() =>
{
string errorMessage = MyCardHelper.DownloadFace(name);
if (errorMessage != null)
Program.PrintToChat(InterString.Get("头像加载失败: ") + errorMessage);
else if (isShowed)
realize();
else if (Program.I().ocgcore.isShowed && Program.I().ocgcore.gameInfo)
Program.I().ocgcore.gameInfo.realize();
})
).Start();
}
// if (Program.I().mycard.isMatching && name != "********") // athletic match name mask
// {
// (
// new Thread(() =>
// {
// string errorMessage = MyCardHelper.DownloadFace(name);
// if (errorMessage != null)
// Program.PrintToChat(InterString.Get("头像加载失败: ") + errorMessage);
// else if (isShowed)
// realize();
// else if (Program.I().ocgcore.isShowed && Program.I().ocgcore.gameInfo)
// Program.I().ocgcore.gameInfo.realize();
// })
// ).Start();
// }
// roomPlayers[pos] = player;
// realize();
// First, add the player and do an initial UI refresh.
// This way, the player's name appears immediately in the UI.
roomPlayers[pos] = player;
realize();
// 注释掉,不下载头像
// Now, if we need to download the face, start the non-blocking coroutine.
// if (Program.I().mycard.isMatching && name != "********")
// {
// // We start a Coroutine from a MonoBehaviour instance, like Program.I()
// Program.I().StartCoroutine(DownloadFaceAndUpdateUI(name));
// }
UIHelper.Flash();
}
private IEnumerator DownloadFaceAndUpdateUI(string playerName)
{
// --- Step 1: Prepare for the background task ---
string downloadErrorMessage = null;
Thread downloadThread = new Thread(() =>
{
// This is the ONLY thing the background thread does: the blocking download.
downloadErrorMessage = MyCardHelper.DownloadFace(playerName);
});
// --- Step 2: Start the background thread and wait for it to finish ---
downloadThread.Start();
// 'yield return' here pauses the coroutine without freezing the game.
// The code will resume only after the downloadThread has completed.
yield return new WaitUntil(() => !downloadThread.IsAlive);
// --- Step 3: We are now back on the MAIN THREAD ---
// It is now safe to use the result and call Unity APIs.
if (downloadErrorMessage != null)
{
Program.PrintToChat(InterString.Get("头像加载失败: ") + downloadErrorMessage);
}
else
{
// The face has downloaded successfully. Now refresh the UI to show it.
if (isShowed)
realize();
else if (Program.I().ocgcore.isShowed && Program.I().ocgcore.gameInfo != null)
Program.I().ocgcore.gameInfo.realize();
}
}
public void StocMessage_Chat(BinaryReader r)
{
int player = r.ReadInt16();
......@@ -372,49 +413,49 @@ public class Room : WindowServantSP
switch (room_status)
{
case 0:
{
hoststr =
"[EFD334][未开始][FFFFFF] "
+ strings[11]
+ "[FFFFFF]"
+ strings[5]
+ " VS "
+ strings[6];
break;
}
{
hoststr =
"[EFD334][未开始][FFFFFF] "
+ strings[11]
+ "[FFFFFF]"
+ strings[5]
+ " VS "
+ strings[6];
break;
}
case 1:
{
hoststr =
"[A978ED][G:"
+ strings[0]
+ ",T:"
+ strings[1]
+ "][FFFFFF] "
+ strings[11]
+ "[FFFFFF]"
+ strings[5]
+ " VS "
+ strings[6];
break;
}
{
hoststr =
"[A978ED][G:"
+ strings[0]
+ ",T:"
+ strings[1]
+ "][FFFFFF] "
+ strings[11]
+ "[FFFFFF]"
+ strings[5]
+ " VS "
+ strings[6];
break;
}
case 2:
{
hoststr =
"[A978ED][G:"
+ strings[0]
+ ",Siding][FFFFFF] "
+ strings[11]
+ "[FFFFFF]"
+ strings[5]
+ " VS "
+ strings[6];
break;
}
{
hoststr =
"[A978ED][G:"
+ strings[0]
+ ",Siding][FFFFFF] "
+ strings[11]
+ "[FFFFFF]"
+ strings[5]
+ " VS "
+ strings[6];
break;
}
default:
{
hoststr = String.Empty;
break;
}
{
hoststr = String.Empty;
break;
}
}
strings[9] = hoststr;
roomList.Add(strings);
......
......@@ -2108,7 +2108,7 @@ gameObjectSearch.transform.DOMove(targetPos, 0.6f);
);
YGOSharp.Card data = YGOSharp.CardsManager.Get(item);
safeGogo(
indexOfLogic * 25,
indexOfLogic * 15,
() =>
{
MonoCardInDeckManager card = createCard();
......@@ -2130,7 +2130,7 @@ gameObjectSearch.transform.DOMove(targetPos, 0.6f);
);
YGOSharp.Card data = YGOSharp.CardsManager.Get(item);
safeGogo(
indexOfLogic * 90,
indexOfLogic * 45,
() =>
{
MonoCardInDeckManager card = createCard();
......@@ -2152,7 +2152,7 @@ gameObjectSearch.transform.DOMove(targetPos, 0.6f);
);
YGOSharp.Card data = YGOSharp.CardsManager.Get(item);
safeGogo(
indexOfLogic * 90,
indexOfLogic * 45,
() =>
{
MonoCardInDeckManager card = createCard();
......
......@@ -5,7 +5,7 @@ using System;
public class coiner : MonoBehaviour
{
private int time;
private const float fastDuration = 1.5f;
private const float fastDuration = 1f;
void Start()
{
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment