close-to-working network interface
This commit is contained in:
parent
8566ec1002
commit
0a575bb378
41
client.cpp
41
client.cpp
@ -1,11 +1,6 @@
|
||||
#include <stdio.h>
|
||||
#include "network.h"
|
||||
|
||||
/*
|
||||
struct hostent *getHost()
|
||||
{
|
||||
|
||||
}*/
|
||||
#include "syncdataclient.h"
|
||||
|
||||
int main(int argc, char *argv[])
|
||||
{
|
||||
@ -30,39 +25,15 @@ int main(int argc, char *argv[])
|
||||
exit(-1);
|
||||
}
|
||||
|
||||
SyncDataClient syncData(serverSocket);
|
||||
SyncTrack &track = syncData.getTrack("test");
|
||||
|
||||
puts("recieving...");
|
||||
bool done = false;
|
||||
while (!done)
|
||||
{
|
||||
// look for new commands
|
||||
while (pollRead(serverSocket))
|
||||
{
|
||||
unsigned char cmd = 0;
|
||||
int ret = recv(serverSocket, (char*)&cmd, 1, 0);
|
||||
if (0 == ret)
|
||||
{
|
||||
done = true;
|
||||
break;
|
||||
}
|
||||
else
|
||||
{
|
||||
switch (cmd)
|
||||
{
|
||||
case 1:
|
||||
printf("yes, master!\n");
|
||||
{
|
||||
unsigned char cmd = 0x1;
|
||||
send(serverSocket, (char*)&cmd, 1, 0);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
printf("unknown cmd: %02x\n", cmd);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
putchar('.');
|
||||
// putchar('.');
|
||||
done = syncData.poll();
|
||||
}
|
||||
closesocket(serverSocket);
|
||||
|
||||
|
||||
@ -28,7 +28,6 @@ SOCKET clientConnect(SOCKET serverSocket)
|
||||
|
||||
recv(clientSocket, recievedGreeting, int(strlen(expectedGreeting)), 0);
|
||||
|
||||
fprintf(stderr, "got: \"%s\"\n", recievedGreeting);
|
||||
if (strncmp(expectedGreeting, recievedGreeting, strlen(expectedGreeting)) != 0)
|
||||
{
|
||||
closesocket(clientSocket);
|
||||
|
||||
@ -11,4 +11,10 @@ SOCKET serverConnect(struct sockaddr_in *addr);
|
||||
|
||||
bool pollRead(SOCKET socket);
|
||||
|
||||
enum RemoteCommand {
|
||||
SET_KEY = 0,
|
||||
DELETE_KEY = 1,
|
||||
GET_TRACK = 2,
|
||||
};
|
||||
|
||||
#endif /* NETWORK_H */
|
||||
|
||||
17
syncdata.h
17
syncdata.h
@ -2,6 +2,7 @@
|
||||
|
||||
#include <string>
|
||||
#include <map>
|
||||
#include <vector>
|
||||
|
||||
#include <exception>
|
||||
#include <cmath>
|
||||
@ -68,6 +69,7 @@ public:
|
||||
|
||||
size_t getFrameCount() const
|
||||
{
|
||||
if (keyFrames.empty()) return 0;
|
||||
KeyFrameContainer::const_iterator iter = keyFrames.end();
|
||||
iter--;
|
||||
return iter->first;
|
||||
@ -85,8 +87,11 @@ public:
|
||||
SyncTrack &getTrack(const std::basic_string<TCHAR> &name)
|
||||
{
|
||||
TrackContainer::iterator iter = tracks.find(name);
|
||||
if (iter != tracks.end()) return iter->second;
|
||||
return tracks[name] = SyncTrack();
|
||||
if (iter != tracks.end()) return *actualTracks[iter->second];
|
||||
|
||||
tracks[name] = actualTracks.size();
|
||||
actualTracks.push_back(new SyncTrack());
|
||||
return *actualTracks.back();
|
||||
}
|
||||
|
||||
SyncTrack &getTrack(size_t track)
|
||||
@ -96,13 +101,15 @@ public:
|
||||
|
||||
SyncData::TrackContainer::iterator trackIter = tracks.begin();
|
||||
for (size_t currTrack = 0; currTrack < track; ++currTrack, ++trackIter);
|
||||
return trackIter->second;
|
||||
return *actualTracks[trackIter->second];
|
||||
}
|
||||
|
||||
size_t getTrackCount() const { return tracks.size(); }
|
||||
|
||||
//private:
|
||||
typedef std::map<const std::basic_string<TCHAR>, SyncTrack> TrackContainer;
|
||||
// private:
|
||||
typedef std::map<const std::basic_string<TCHAR>, size_t> TrackContainer;
|
||||
// typedef std::map<const std::basic_string<TCHAR>, SyncTrack> TrackContainer;
|
||||
TrackContainer tracks;
|
||||
// std::vector<SyncTrack*> actualTracks;
|
||||
};
|
||||
|
||||
|
||||
68
syncdataclient.cpp
Normal file
68
syncdataclient.cpp
Normal file
@ -0,0 +1,68 @@
|
||||
#include "syncdataclient.h"
|
||||
#include "network.h"
|
||||
|
||||
SyncTrack &SyncDataClient::getTrack(const std::basic_string<TCHAR> &name)
|
||||
{
|
||||
TrackContainer::iterator iter = tracks.find(name);
|
||||
if (iter != tracks.end()) return iter->second;
|
||||
|
||||
unsigned char cmd = GET_TRACK;
|
||||
send(serverSocket, (char*)&cmd, 1, 0);
|
||||
|
||||
// send request data
|
||||
int name_len = name.size();
|
||||
printf("len: %d\n", name_len);
|
||||
send(serverSocket, (char*)&name_len, sizeof(int), 0);
|
||||
|
||||
const char *name_str = name.c_str();
|
||||
send(serverSocket, name_str, name_len, 0);
|
||||
|
||||
SyncTrack track = SyncTrack();
|
||||
/* todo: fill in based on the response */
|
||||
return tracks[name] = track;
|
||||
}
|
||||
|
||||
bool SyncDataClient::poll()
|
||||
{
|
||||
bool done = false;
|
||||
// look for new commands
|
||||
while (pollRead(serverSocket))
|
||||
{
|
||||
unsigned char cmd = 0;
|
||||
int ret = recv(serverSocket, (char*)&cmd, 1, 0);
|
||||
if (0 == ret)
|
||||
{
|
||||
done = true;
|
||||
break;
|
||||
}
|
||||
else
|
||||
{
|
||||
switch (cmd)
|
||||
{
|
||||
case SET_KEY:
|
||||
{
|
||||
int track, row;
|
||||
float value;
|
||||
recv(serverSocket, (char*)&track, sizeof(int), 0);
|
||||
recv(serverSocket, (char*)&row, sizeof(int), 0);
|
||||
recv(serverSocket, (char*)&value, sizeof(float), 0);
|
||||
printf("set: %d,%d = %f\n", track, row, value);
|
||||
}
|
||||
break;
|
||||
|
||||
case DELETE_KEY:
|
||||
{
|
||||
int track, row;
|
||||
recv(serverSocket, (char*)&track, sizeof(int), 0);
|
||||
recv(serverSocket, (char*)&row, sizeof(int), 0);
|
||||
printf("delete: %d,%d = %f\n", track, row);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
printf("unknown cmd: %02x\n", cmd);
|
||||
}
|
||||
}
|
||||
}
|
||||
return done;
|
||||
}
|
||||
14
syncdataclient.h
Normal file
14
syncdataclient.h
Normal file
@ -0,0 +1,14 @@
|
||||
#include "network.h"
|
||||
#include "syncdata.h"
|
||||
|
||||
class SyncDataClient : public SyncData
|
||||
{
|
||||
public:
|
||||
SyncDataClient(SOCKET serverSocket) : serverSocket(serverSocket) {}
|
||||
|
||||
SyncTrack &getTrack(const std::basic_string<TCHAR> &name);
|
||||
bool poll();
|
||||
private:
|
||||
std::map<int, SyncTrack*> serverRemap;
|
||||
SOCKET serverSocket;
|
||||
};
|
||||
@ -4,11 +4,35 @@
|
||||
#include <stack>
|
||||
#include <list>
|
||||
|
||||
#include "network.h"
|
||||
|
||||
class SyncEditData : public SyncData
|
||||
{
|
||||
public:
|
||||
SyncEditData() : SyncData() {}
|
||||
|
||||
void sendSetKeyCommand(int track, int row, const SyncTrack::KeyFrame &key)
|
||||
{
|
||||
if (INVALID_SOCKET == clientSocket) return;
|
||||
|
||||
unsigned char cmd = SET_KEY;
|
||||
send(clientSocket, (char*)&cmd, 1, 0);
|
||||
send(clientSocket, (char*)&track, sizeof(int), 0);
|
||||
send(clientSocket, (char*)&row, sizeof(int), 0);
|
||||
send(clientSocket, (char*)&key.value, sizeof(float), 0);
|
||||
// send(clientSocket, (char*)&key.lerp, 1, 0);
|
||||
}
|
||||
|
||||
void sendDeleteKeyCommand(int track, int row)
|
||||
{
|
||||
if (INVALID_SOCKET == clientSocket) return;
|
||||
|
||||
unsigned char cmd = DELETE_KEY;
|
||||
send(clientSocket, (char*)&cmd, 1, 0);
|
||||
send(clientSocket, (char*)&track, sizeof(int), 0);
|
||||
send(clientSocket, (char*)&row, sizeof(int), 0);
|
||||
}
|
||||
|
||||
class Command
|
||||
{
|
||||
public:
|
||||
@ -20,51 +44,59 @@ public:
|
||||
class InsertCommand : public Command
|
||||
{
|
||||
public:
|
||||
InsertCommand(size_t track, size_t row, const SyncTrack::KeyFrame &key) : track(track), row(row), key(key) {}
|
||||
InsertCommand(int track, int row, const SyncTrack::KeyFrame &key) : track(track), row(row), key(key) {}
|
||||
~InsertCommand() {}
|
||||
|
||||
virtual void exec(SyncEditData *data)
|
||||
{
|
||||
SyncTrack &track = data->getTrack(this->track);
|
||||
assert(!track.isKeyFrame(row));
|
||||
track.setKeyFrame(row, key);
|
||||
SyncTrack &t = data->getTrack(this->track);
|
||||
assert(!t.isKeyFrame(row));
|
||||
t.setKeyFrame(row, key);
|
||||
|
||||
data->sendSetKeyCommand(track, row, key); // update clients
|
||||
}
|
||||
|
||||
virtual void undo(SyncEditData *data)
|
||||
{
|
||||
SyncTrack &track = data->getTrack(this->track);
|
||||
assert(track.isKeyFrame(row));
|
||||
track.deleteKeyFrame(row);
|
||||
SyncTrack &t = data->getTrack(this->track);
|
||||
assert(t.isKeyFrame(row));
|
||||
t.deleteKeyFrame(row);
|
||||
|
||||
data->sendDeleteKeyCommand(track, row); // update clients
|
||||
}
|
||||
|
||||
private:
|
||||
size_t track, row;
|
||||
int track, row;
|
||||
SyncTrack::KeyFrame key;
|
||||
};
|
||||
|
||||
class DeleteCommand : public Command
|
||||
{
|
||||
public:
|
||||
DeleteCommand(size_t track, size_t row) : track(track), row(row) {}
|
||||
DeleteCommand(int track, int row) : track(track), row(row) {}
|
||||
~DeleteCommand() {}
|
||||
|
||||
virtual void exec(SyncEditData *data)
|
||||
{
|
||||
SyncTrack &track = data->getTrack(this->track);
|
||||
assert(track.isKeyFrame(row));
|
||||
oldKey = *track.getKeyFrame(row);
|
||||
track.deleteKeyFrame(row);
|
||||
SyncTrack &t = data->getTrack(this->track);
|
||||
assert(t.isKeyFrame(row));
|
||||
oldKey = *t.getKeyFrame(row);
|
||||
t.deleteKeyFrame(row);
|
||||
|
||||
data->sendDeleteKeyCommand(track, row); // update clients
|
||||
}
|
||||
|
||||
virtual void undo(SyncEditData *data)
|
||||
{
|
||||
SyncTrack &track = data->getTrack(this->track);
|
||||
assert(!track.isKeyFrame(row));
|
||||
track.setKeyFrame(row, oldKey);
|
||||
SyncTrack &t = data->getTrack(this->track);
|
||||
assert(!t.isKeyFrame(row));
|
||||
t.setKeyFrame(row, oldKey);
|
||||
|
||||
data->sendSetKeyCommand(track, row, oldKey); // update clients
|
||||
}
|
||||
|
||||
private:
|
||||
size_t track, row;
|
||||
int track, row;
|
||||
SyncTrack::KeyFrame oldKey;
|
||||
};
|
||||
|
||||
@ -72,31 +104,35 @@ public:
|
||||
class EditCommand : public Command
|
||||
{
|
||||
public:
|
||||
EditCommand(size_t track, size_t row, const SyncTrack::KeyFrame &key) : track(track), row(row), key(key) {}
|
||||
EditCommand(int track, int row, const SyncTrack::KeyFrame &key) : track(track), row(row), key(key) {}
|
||||
~EditCommand() {}
|
||||
|
||||
virtual void exec(SyncEditData *data)
|
||||
{
|
||||
SyncTrack &track = data->getTrack(this->track);
|
||||
SyncTrack &t = data->getTrack(this->track);
|
||||
|
||||
// store old key
|
||||
assert(track.isKeyFrame(row));
|
||||
oldKey = *track.getKeyFrame(row);
|
||||
assert(t.isKeyFrame(row));
|
||||
oldKey = *t.getKeyFrame(row);
|
||||
|
||||
// update
|
||||
track.setKeyFrame(row, key);
|
||||
t.setKeyFrame(row, key);
|
||||
|
||||
data->sendSetKeyCommand(track, row, key); // update clients
|
||||
}
|
||||
|
||||
virtual void undo(SyncEditData *data)
|
||||
{
|
||||
SyncTrack &track = data->getTrack(this->track);
|
||||
SyncTrack &t = data->getTrack(this->track);
|
||||
|
||||
assert(track.isKeyFrame(row));
|
||||
track.setKeyFrame(row, oldKey);
|
||||
assert(t.isKeyFrame(row));
|
||||
t.setKeyFrame(row, oldKey);
|
||||
|
||||
data->sendSetKeyCommand(track, row, oldKey); // update clients
|
||||
}
|
||||
|
||||
private:
|
||||
size_t track, row;
|
||||
int track, row;
|
||||
SyncTrack::KeyFrame oldKey, key;
|
||||
};
|
||||
|
||||
@ -186,8 +222,9 @@ public:
|
||||
return cmd;
|
||||
}
|
||||
|
||||
|
||||
SOCKET clientSocket;
|
||||
private:
|
||||
// std::map<SyncTrack*, int> clientRemap;
|
||||
|
||||
std::stack<Command*> undoStack;
|
||||
std::stack<Command*> redoStack;
|
||||
|
||||
@ -6,7 +6,7 @@
|
||||
#include <commctrl.h>
|
||||
|
||||
#include "trackview.h"
|
||||
|
||||
#include <vector>
|
||||
const TCHAR *mainWindowClassName = _T("MainWindow");
|
||||
|
||||
TrackView *trackView;
|
||||
@ -282,16 +282,23 @@ int _tmain(int argc, _TCHAR* argv[])
|
||||
#endif
|
||||
|
||||
SyncEditData syncData;
|
||||
syncData.clientSocket = INVALID_SOCKET;
|
||||
|
||||
SyncTrack &camXTrack = syncData.getTrack(_T("cam.x"));
|
||||
SyncTrack &camXTrack2 = syncData.getTrack(_T("cam.x"));
|
||||
camXTrack.setKeyFrame(1, 2.0f);
|
||||
camXTrack.setKeyFrame(4, 3.0f);
|
||||
printf("%p %p\n", &camXTrack, &camXTrack2);
|
||||
|
||||
SyncTrack &camYTrack = syncData.getTrack(_T("cam.y"));
|
||||
SyncTrack &camZTrack = syncData.getTrack(_T("cam.z"));
|
||||
|
||||
for (int i = 0; i < 16; ++i)
|
||||
/* for (int i = 0; i < 16; ++i)
|
||||
{
|
||||
TCHAR temp[256];
|
||||
_sntprintf_s(temp, 256, _T("gen %02d"), i);
|
||||
SyncTrack &temp2 = syncData.getTrack(temp);
|
||||
}
|
||||
} */
|
||||
|
||||
camXTrack.setKeyFrame(1, 2.0f);
|
||||
camXTrack.setKeyFrame(4, 3.0f);
|
||||
@ -368,31 +375,64 @@ int _tmain(int argc, _TCHAR* argv[])
|
||||
clientSocket = clientConnect(serverSocket);
|
||||
if (INVALID_SOCKET != clientSocket)
|
||||
{
|
||||
unsigned char cmd = 0x1;
|
||||
send(clientSocket, (char*)&cmd, 1, 0);
|
||||
puts("connected.");
|
||||
syncData.clientSocket = clientSocket;
|
||||
/* for (int track = 0; track < syncData.getTrackCount(); ++track)
|
||||
{
|
||||
|
||||
} */
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
unsigned char cmd = 0x1;
|
||||
send(clientSocket, (char*)&cmd, 1, 0);
|
||||
|
||||
// look for new commands
|
||||
while (pollRead(clientSocket))
|
||||
{
|
||||
unsigned char cmd = 0;
|
||||
int ret = recv(clientSocket, (char*)&cmd, 1, 0);
|
||||
if (1 != ret)
|
||||
if (1 > ret)
|
||||
{
|
||||
closesocket(clientSocket);
|
||||
clientSocket = INVALID_SOCKET;
|
||||
syncData.clientSocket = INVALID_SOCKET;
|
||||
break;
|
||||
}
|
||||
else
|
||||
{
|
||||
printf("cmd: %02x\n", cmd);
|
||||
if (cmd == 1) printf("yes, master!\n");
|
||||
switch (cmd)
|
||||
{
|
||||
case GET_TRACK:
|
||||
// get len
|
||||
int str_len = 0;
|
||||
int ret = recv(clientSocket, (char*)&str_len, sizeof(int), 0);
|
||||
assert(ret == sizeof(size_t));
|
||||
printf("len: %d\n", str_len);
|
||||
|
||||
// int clientAddr = 0;
|
||||
// int ret = recv(clientSocket, (char*)&clientAddr, sizeof(int), 0);
|
||||
|
||||
// get string
|
||||
std::string trackName;
|
||||
trackName.resize(str_len * 2);
|
||||
recv(clientSocket, &trackName[0], str_len, 0);
|
||||
trackName.push_back('\0');
|
||||
|
||||
//
|
||||
printf("name: %s\n", trackName.c_str());
|
||||
|
||||
const SyncTrack &track = syncData.getTrack(trackName);
|
||||
// clientRemap[track] = clientAddr;
|
||||
|
||||
for (size_t keyframe = 0; keyframe < track.getFrameCount(); ++keyframe)
|
||||
{
|
||||
// printf("name: %s\n", trackName.c_str());
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
// printf("cmd: %02x\n", cmd);
|
||||
// if (cmd == 1) printf("yes, master!\n");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -260,7 +260,7 @@ void TrackView::paintTracks(HDC hdc, RECT rcTracks)
|
||||
|
||||
// InvertRect(hdc, &fillRect);
|
||||
|
||||
const SyncTrack &track = trackIter->second;
|
||||
const SyncTrack &track = *syncData->actualTracks[trackIter->second];
|
||||
bool key = track.isKeyFrame(row);
|
||||
|
||||
/* format the text */
|
||||
@ -325,16 +325,6 @@ void TrackView::editCopy()
|
||||
int selectTop = min(selectStartRow, selectStopRow);
|
||||
int selectBottom = max(selectStartRow, selectStopRow);
|
||||
|
||||
#if 0
|
||||
struct CopyEntry
|
||||
{
|
||||
int track, row;
|
||||
float val;
|
||||
bool valExisting;
|
||||
};
|
||||
std::vector<CopyEntry> copyBuffer;
|
||||
#endif
|
||||
|
||||
if (FAILED(OpenClipboard(getWin())))
|
||||
{
|
||||
MessageBox(NULL, _T("Failed to open clipboard"), NULL, MB_OK);
|
||||
@ -346,17 +336,14 @@ void TrackView::editCopy()
|
||||
int columns = selectRight - selectLeft + 1;
|
||||
size_t cells = columns * rows;
|
||||
|
||||
std::string copyString;
|
||||
|
||||
std::vector<struct CopyEntry> copyEntries;
|
||||
for (int row = selectTop; row <= selectBottom; ++row)
|
||||
for (int track = selectLeft; track <= selectRight; ++track)
|
||||
{
|
||||
int localRow = row - selectTop;
|
||||
for (int track = selectLeft; track <= selectRight; ++track)
|
||||
int localTrack = track - selectLeft;
|
||||
const SyncTrack &t = syncData->getTrack(track);
|
||||
for (int row = selectTop; row <= selectBottom; ++row)
|
||||
{
|
||||
int localTrack = track - selectLeft;
|
||||
const SyncTrack &t = syncData->getTrack(track);
|
||||
char temp[128];
|
||||
int localRow = row - selectTop;
|
||||
if (t.isKeyFrame(row))
|
||||
{
|
||||
const SyncTrack::KeyFrame *keyFrame = t.getKeyFrame(row);
|
||||
@ -368,12 +355,8 @@ void TrackView::editCopy()
|
||||
ce.keyFrame = *keyFrame;
|
||||
|
||||
copyEntries.push_back(ce);
|
||||
sprintf(temp, "%.2f\t", keyFrame->value);
|
||||
}
|
||||
else sprintf(temp, "--- \t");
|
||||
copyString += temp;
|
||||
}
|
||||
copyString += "\n";
|
||||
}
|
||||
|
||||
int buffer_width = selectRight - selectLeft + 1;
|
||||
@ -392,16 +375,9 @@ void TrackView::editCopy()
|
||||
GlobalUnlock(hmem);
|
||||
clipbuf = NULL;
|
||||
|
||||
HGLOBAL hmem_text = GlobalAlloc(GMEM_MOVEABLE, strlen(copyString.c_str()) + 1);
|
||||
clipbuf = (char *)GlobalLock(hmem_text);
|
||||
memcpy(clipbuf, copyString.c_str(), strlen(copyString.c_str()) + 1);
|
||||
GlobalUnlock(hmem_text);
|
||||
clipbuf = NULL;
|
||||
|
||||
// update clipboard
|
||||
EmptyClipboard();
|
||||
SetClipboardData(clipboardFormat, hmem);
|
||||
/* SetClipboardData(CF_TEXT, hmem_text); */
|
||||
CloseClipboard();
|
||||
}
|
||||
|
||||
@ -451,16 +427,6 @@ void TrackView::editPaste()
|
||||
GlobalUnlock(hmem);
|
||||
clipbuf = NULL;
|
||||
}
|
||||
else if (!IsClipboardFormatAvailable(clipboardFormat))
|
||||
{
|
||||
HGLOBAL hmem = GetClipboardData(clipboardFormat);
|
||||
char *clipbuf = (char *)GlobalLock(hmem);
|
||||
|
||||
/* DO STUFFZ! */
|
||||
|
||||
GlobalUnlock(hmem);
|
||||
clipbuf = NULL;
|
||||
}
|
||||
else MessageBeep(0);
|
||||
|
||||
CloseClipboard();
|
||||
@ -761,19 +727,6 @@ void TrackView::editBiasValue(float amount)
|
||||
syncData->exec(multiCmd);
|
||||
invalidateRange(selectLeft, selectRight, selectTop, selectBottom);
|
||||
}
|
||||
|
||||
/* SyncTrack &track = syncData->getTrack(editTrack);
|
||||
if (track.isKeyFrame(editRow))
|
||||
{
|
||||
SyncTrack::KeyFrame newKey = *track.getKeyFrame(editRow);
|
||||
newKey.value += amount;
|
||||
|
||||
SyncEditData::Command *cmd = syncData->getSetKeyFrameCommand(editTrack, editRow, newKey);
|
||||
syncData->exec(cmd);
|
||||
|
||||
invalidatePos(editTrack, editRow);
|
||||
}
|
||||
else MessageBeep(0); */
|
||||
}
|
||||
|
||||
LRESULT TrackView::onKeyDown(UINT keyCode, UINT /*flags*/)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user