Creating unique callback references - c#

So the problem is as follows: I'm using a third party client class to issue commands to an external system to retrieve data (fairly standard). The problem is that when I issue commands via an instance of this class, it uses a callback reference based on the function name in my code and the line number within that function from which it was called, it then serializes this and other information into JSON and transmits to the external system for processing, with the data return being identified by the reference. This works "well" until we get to iteration, then the callback reference remains the same and I only receive data for one iteration. The third party isn't going to alter their code so I need a way of generating unique references in my code, but I'm unsure of how I can do this within C#. I can't edit their class as it is provided as a DLL, I can only access this system by using it (it is part of their SDK).
Any ideas greatly appreciated!
Example code:
[Note: actual code is part of a Windows Form Application]
The last part client.IsUserInGroup is the problem.
using thirdParty.Client;
class TestProgram
{
static void Main(string[] args)
{
//Area and user objects defined within third party class
List<Area> Areas = new List<Area>();
List<User> myUsers = new List<User>();
int publicAreaID = 0;
bool isConnected=false;
client.Connect("user", "pass",
(connstatus) =>
{
switch (connstatus)
{
case ConnectionStatus.Success:
isConnected = true;
Console.WriteLine("Connected");
break;
case ConnectionStatus.InvalidCredentials:
Console.WriteLine("InvalidCredentials");
break;
case ConnectionStatus.Timeout:
Console.WriteLine("Timeout");
break;
}
});
if (isConnected)
{
client.GetAreas(
(result) =>
{
Areas = result;
});
//Get ID of public area
foreach (Area myArea in Areas)
{
if (myArea.Name.Equals("Public"))
{
publicAreaID = myArea.ID;
}
}
//Get all keyholders in Public area and store in list
client.GetUsersInArea(publicAreaID,
(result) =>
{
myUsers = result;
});
//Iterate over all users in list and verify they are in the everyone group
foreach (User myUser in myUsers)
{
User tempUser = myUser;
client.IsUserInGroup(tempUser.ID, 0,
(result) =>
{
if (result) //this is a bool
{
//This only returns one result..
Console.WriteLine(String.Format("{0} is in Everyone Group and Public Area", tempUser.Name));
}
});
}
client.Disconnect();
}
}
}
UPDATE
I've been doing more testing by removing the foreach loop and just calling client.IsUserInGroup twice to generate alternative callback references; the results are interesting. As expected there are unique references, but there is still only one result displayed, the only way to get both is to create two User objects rather than reuse one. As mentioned above, the "real" code is used in a Windows forms app, could this be something to do with object referencing? Example code below:
new User tempUser1 = myUsers[0];
client.IsUserInGroup(tempUser1.ID, 0,
(result) =>
{
if (result) //this is a bool
{
Console.WriteLine(String.Format("{0} is in Everyone Group and Public Area", tempUser1.Name));
}
});
new User tempUser2 = myUsers[1];
client.IsUserInGroup(tempUser2.ID, 0,
(result) =>
{
if (result) //this is a bool
{
Console.WriteLine(String.Format("{0} is in Everyone Group and Public Area", tempUser2.Name));
}
});

Answer moved from OP's original question:
Ok so I was playing with this a lot over the last few hours and kind of made it work by keeping the iterative loop but doing two things; firstly I assumed that the third party class would synchronize information requests and not allow my code to continue until it had a result returned - this seemingly is not the case as the output from recursion with an extra Console.WriteLine(iterationCount) in it shows the count increasing with no data returned; therefore I am forced to slow down the code by Thread.Sleep (I'm investigating better ways of doing this). Secondly any code within the lambda that could be moved outside, was. Instead a temp bool outside of the lambda was assigned the value of the result bool. The code looks like:
//Iterate over all users in list and verify they are in the everyone group
foreach (User myUser in myUsers)
{
User tempUser = myUser;
bool tempRes = false;
client.IsUserInGroup(tempUser.ID, 0,
(result) =>
{
tempRes = result;
});
if (tempRes)
{
Console.WriteLine(String.Format("{0} is in Everyone Group and Public Area", tempUser.Name));
}
System.Threading.Thread.Sleep(75); //Not a good way of enforcing sync!
}

Related

Block Controller Method while already running

I have a controller which returns a large json object. If this object does not exist, it will generate and return it afterwards. The generation takes about 5 seconds, and if the client sent the request multiple times, the object gets generated with x-times the children. So my question is: Is there a way to block the second request, until the first one finished, independent who sent the request?
Normally I would do it with a Singleton, but because I am having scoped services, singleton does not work here
Warning: this is very oppinionated and maybe not suitable for Stack Overflow, but here it is anyway
Although I'll provide no code... when things take a while to generate, you don't usually spend that time directly in controller code, but do something like "start a background task to generate the result, and provide a "task id", which can be queried on another different call).
So, my preferred course of action for this would be having two different controller actions:
Generate, which creates the background job, assigns it some id, and returns the id
GetResult, to which you pass the task id, and returns either different error codes for "job id doesn't exist", "job id isn't finished", or a 200 with the result.
This way, your clients will need to call both, however, in Generate, you can check if the job is already being created and return an existing job id.
This of course moves the need to "retry and check" to your client: in exchange, you don't leave the connection to the server opened during those 5 seconds (which could potentially be multiplied by a number of clients) and return fast.
Otherwise, if you don't care about having your clients wait for a response during those 5 seconds, you could do a simple:
if(resultDoesntExist) {
resultDoesntExist = false; // You can use locks for the boolean setters or Interlocked instead of just setting a member
resultIsBeingGenerated = true;
generateResult(); // <-- this is what takes 5 seconds
resultIsBeingGenerated = false;
}
while(resultIsBeingGenerated) { await Task.Delay(10); } // <-- other clients will wait here
var result = getResult(); // <-- this should be fast once the result is already created
return result;
note: those booleans and the actual loop could be on the controller, or on the service, or wherever you see fit: just be wary of making them thread-safe in however method you see appropriate
So you basically make other clients wait till the first one generates the result, with "almost" no CPU load on the server... however with a connection open and a thread from the threadpool used, so I just DO NOT recommend this :-)
PS: #Leaky solution above is also good, but it also shifts the responsability to retry to the client, and if you are going to do that, I'd probably go directly with a "background job id", instead of having the first (the one that generates the result) one take 5 seconds. IMO, if it can be avoided, no API action should ever take 5 seconds to return :-)
Do you have an example for Interlocked.CompareExchange?
Sure. I'm definitely not the most knowledgeable person when it comes to multi-threading stuff, but this is quite simple (as you might know, Interlocked has no support for bool, so it's customary to represent it with an integral type):
public class QueryStatus
{
private static int _flag;
// Returns false if the query has already started.
public bool TrySetStarted()
=> Interlocked.CompareExchange(ref _flag, 1, 0) == 0;
public void SetFinished()
=> Interlocked.Exchange(ref _flag, 0);
}
I think it's the safest if you use it like this, with a 'Try' method, which tries to set the value and tells you if it was already set, in an atomic way.
Besides simply adding this (I mean just the field and the methods) to your existing component, you can also use it as a separate component, injected from the IOC container as scoped. Or even injected as a singleton, and then you don't have to use a static field.
Storing state like this should be good for as long as the application is running, but if the hosted application is recycled due to inactivity, it's obviously lost. Though, that won't happen while a request is still processing, and definitely won't happen in 5 seconds.
(And if you wanted to synchronize between app service instances, you could 'quickly' save a flag to the database, in a transaction with proper isolation level set. Or use e.g. Azure Redis Cache.)
Example solution
As Kit noted, rightly so, I didn't provide a full solution above.
So, a crude implementation could go like this:
public class SomeQueryService : ISomeQueryService
{
private static int _hasStartedFlag;
private static bool TrySetStarted()
=> Interlocked.CompareExchange(ref _hasStartedFlag, 1, 0) == 0;
private static void SetFinished()
=> Interlocked.Exchange(ref _hasStartedFlag, 0);
public async Task<(bool couldExecute, object result)> TryExecute()
{
if (!TrySetStarted())
return (couldExecute: false, result: null);
// Safely execute long query.
SetFinished();
return (couldExecute: true, result: result);
}
}
// In the controller, obviously
[HttpGet()]
public async Task<IActionResult> DoLongQuery([FromServices] ISomeQueryService someQueryService)
{
var (couldExecute, result) = await someQueryService.TryExecute();
if (!couldExecute)
{
return new ObjectResult(new ProblemDetails
{
Status = StatusCodes.Status503ServiceUnavailable,
Title = "Another request has already started. Try again later.",
Type = "https://tools.ietf.org/html/rfc7231#section-6.6.4"
})
{ StatusCode = StatusCodes.Status503ServiceUnavailable };
}
return Ok(result);
}
Of course possibly you'd want to extract the 'blocking' logic from the controller action into somewhere else, for example an action filter. In that case the flag should also go into a separate component that could be shared between the query service and the filter.
General use action filter
I felt bad about my inelegant solution above, and I realized that this problem can be generalized into basically a connection number limiter on an endpoint.
I wrote this small action filter that can be applied to any endpoint (multiple endpoints), and it accepts the number of allowed connections:
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
public class ConcurrencyLimiterAttribute : ActionFilterAttribute
{
private readonly int _allowedConnections;
private static readonly ConcurrentDictionary<string, int> _connections = new ConcurrentDictionary<string, int>();
public ConcurrencyLimiterAttribute(int allowedConnections = 1)
=> _allowedConnections = allowedConnections;
public override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
var key = context.HttpContext.Request.Path;
if (_connections.AddOrUpdate(key, 1, (k, v) => ++v) > _allowedConnections)
{
Close(withError: true);
return;
}
try
{
await next();
}
finally
{
Close();
}
void Close(bool withError = false)
{
if (withError)
{
context.Result = new ObjectResult(new ProblemDetails
{
Status = StatusCodes.Status503ServiceUnavailable,
Title = $"Maximum {_allowedConnections} simultaneous connections are allowed. Try again later.",
Type = "https://tools.ietf.org/html/rfc7231#section-6.6.4"
})
{ StatusCode = StatusCodes.Status503ServiceUnavailable };
}
_connections.AddOrUpdate(key, 0, (k, v) => --v);
}
}
}

How to get the latest ID from Firebase console from Untiy

I have a Unity project with Firebase for google Authentication. I am storing user information in a realtime database. They are stored as:
UnityProject
1->
Name: "Something"
Email: "Something"
2->
Name: "some other thing"
Email: "Something else"
Now, these 1,2 are to be given by me (to be used as a primary key). I would start by giving the first user ID 1 and the second used ID 2 and so on.
But I need to get back from the firebase the last ID which was stored. For example,
idToInsert = GetLastUsedID() + 1;
I have used this code but it doesn't work. The screen just freezes until I force close Unity.
public int GetLastUsedID()
{
int currentID = 1;
bool continueSearch = true;
while (continueSearch)
{
FirebaseREST.DatabaseReference reference = FirebaseREST.FirebaseDatabase.Instance.GetReference(""+currentID);
string value = "";
currentID++;
reference.GetValueAsync(10, (res) =>
{
if (res.success)
{
value = res.data.GetRawJsonValue();
Debug.Log("Success fetched data : " + value);
if(value == "")
{
continueSearch = false;
Debug.Log(currentID);
}
}
else
{
Debug.Log("Fetch data failed : " + res.message);
continueSearch = false;
}
});
}
return currentID;
}
Basically I am just trying to iterate from 1 till whenever I get the empty string. The empty string means no data exists under that ID.
I'm not familiar with FirebaseREST (I would recommend using the official Firebase plugin if you're able, it does much more than just call REST endpoints), but I think I can see your issue.
GetValueAsync likely runs asynchronously in the background. So the logic inside the block (the (res)=> part) is firing off many times (basically infinitely with that while loop). Then, depending on how this is implemented, either continueSearch never goes to false because it isn't marked volatile or the callback logic never gets a chance to run (say if FirebaseREST tries to dispatch to the main thread, which is locked in an infinite while loop).
If GetValueAsync returns a Task, you can use some of the tips I cover in this article. My recommendation would be to try to use async/await, so your logic would look more like:
async public int GetLastUsedID()
{
volatile int currentID = 1;
volatile bool continueSearch = true;
while (continueSearch)
{
FirebaseREST.DatabaseReference reference = FirebaseREST.FirebaseDatabase.Instance.GetReference(""+currentID);
string value = "";
currentID++;
await reference.GetValueAsync(10, (res) =>
{
if (res.success)
{
value = res.data.GetRawJsonValue();
Debug.Log("Success fetched data : " + value);
if(value == "")
{
continueSearch = false;
Debug.Log(currentID);
}
}
else
{
Debug.Log("Fetch data failed : " + res.message);
continueSearch = false;
}
});
}
return currentID;
}
This is probably not the case, in which case you'll probably want to turn this into a recursive call. You'd want some function like:
public void GetLastUsedID(currentId)
{
FirebaseREST.DatabaseReference reference = FirebaseREST.FirebaseDatabase.Instance.GetReference(""+currentID);
reference.GetValueAsync(10, (res) =>
{
if (res.success)
{
value = res.data.GetRawJsonValue();
Debug.Log("Success fetched data : " + value);
if(value == "")
{
continueSearch = false;
Debug.Log(currentID);
}
else
{
// recurse
GetLastID(currentId+1);
}
}
else
{
Debug.Log("Fetch data failed : " + res.message);
continueSearch = false;
}
});
}
You will have to figure out how to pass in your own callback to bubble a success or failure like this (exercise to the reader).
Finally, I would be a little cautious with what you're doing entirely. If you're authenticating users, I'd recommend using Firebase Authentication which ties directly into Realtime Database. At most you'd only store user id's in there, and use security rules to ensure that only that user can write into it (for instance). By using Firebase Authentication to manage user data and tying that to RTDB rules, you avoid the same mistakes this author made.

How to join results on a firebase query the right way

I am using Firebase real-time database for my app build with Unity. In order to build a "friend" leaderboard, the database will keep track of users their friends and their scores.
The database has the following structure:
scores{
user id : score
}
Users:{
Id: {
ageRange
email
name
friendlist : {
multiple friends user ids
}
}
}
The problem is in order to get the scores and the names of every friend the app has to make alot of api calls. Atleast if I correctly understand firebase. If the user has 10 friends it will take 21 calls before the leaderboard is filled.
I came up with the following code written in c#:
List<UserScore> leaderBoard = new List<UserScore>();
db.Child("users").Child(uid).Child("friendList").GetValueAsync().ContinueWith(task => {
if (task.IsCompleted)
{
//foreach friend
foreach(DataSnapshot h in task.Result.Children)
{
string curName ="";
int curScore = 0;
//get his name in the user table
db.Child("users").Child(h.Key).GetValueAsync().ContinueWith(t => {
if (t.IsCompleted)
{
DataSnapshot s = t.Result;
curName = s.Child("name").Value.ToString();
//get his score from the scores table
db.Child("scores").Child(h.Key).GetValueAsync().ContinueWith(q => {
if (q.IsCompleted)
{
DataSnapshot b = q.Result;
curScore = int.Parse(b.Value.ToString());
//make new userscore and add to leaderboard
leaderBoard.Add(new UserScore(curName, curScore));
Debug.Log(curName);
Debug.Log(curScore.ToString());
}
});
}
});
}
}
});
Is there any other way to do this? I've read multiple stack overflow questions watched firebase tutorials but i didnt found any simpler or more efficient way to get the job done.
There's not a way of reducing the number of API calls without duplicating data/restructuring your database. However, reducing API calls doesn't necessarily mean the overall read strategy is faster/better. My suggestion would be to optimize your reading strategy to reduce overall data read and to make reads concurrently when possible.
Solution 1: Optimize reading strategy
This is my recommended solution, because it doesn't include extra unnecessary data nor does it include managing consistency of your data.
Your code should look something like below:
(DISCLAIMER: I'm not a C# programmer, so there might be some errors)
List<UserScore> leaderBoard = new List<UserScore>();
db.Child("users").Child(uid).Child("friendList").GetValueAsync().ContinueWith(task => {
if (task.IsCompleted)
{
//foreach friend
foreach(DataSnapshot h in task.Result.Children)
{
// kick off the task to retrieve friend's name
Task nameTask = db.Child("users").Child(h.Key).Child("name").GetValueAsync();
// kick off the task to retrieve friend's score
Task scoreTask = db.Child("scores").Child(h.Key).GetValueAsync();
// join tasks into one final task
Task finalTask = Task.Factory.ContinueWhenAll((new[] {nameTask, scoreTask}), tasks => {
if (nameTask.IsCompleted && scoreTask.IsCompleted) {
// both tasks are complete; add new record to leaderboard
string name = nameTask.Result.Value.ToString();
int score = int.Parse(scoreTask.Result.Value.ToString());
leaderBoard.Add(new UserScore(name, score));
Debug.Log(name);
Debug.Log(score.ToString());
}
})
}
}
});
The above code improves the overall read strategy by not pulling all of a friend's user data (i.e. name, email, friendlist, etc.) and by pulling the name concurrently with score.
Solution 2: Duplicate name to scores table
If this still isn't optimal enough, you can always duplicate the friend's name in their score table. Something like below:
scores: {
<user_id>: {
name: <user_name>,
score: <user_score>
}
}
This would then allow you to only make one call per friend instead of two. However, you will still be reading the same amount of data, and you will have to manage the consistency of the data (either use a Firebase Function to propagate user name changes or write to both places).
Solution 3: Combine scores table into users table
If you don't want to manage the consistency issue, you can just combine the scores table into the users table.
Your structure would be something like:
users: {
<user_id>: {
name: <user_name>,
...,
score: <user_score>
}
}
However, in this instance, you will be reading more data (email, friendlist, etc.)
I hope this helps.
While the following will not reduce the number of calls, it will create tasks for the retrieval of the data and run them all simultaneously, returning the list of desired user scores.
var friendList = await db.Child("users").Child(uid).Child("friendList").GetValueAsync();
List<Task<UserScore>> tasks = new List<Task<UserScore>>();
//foreach friend
foreach(DataSnapshot friend in friendList.Children) {
var task = Task.Run( async () => {
var friendKey = friend.Key;
//get his name in the user table
var getName = db.Child("users").Child(friendKey).Child("name").GetValueAsync();
//get his score from the scores table
var getScore = db.Child("scores").Child(friendKey).GetValueAsync();
await Task.WhenAll(getName, getScore);
var name = getName.Result.Value.ToString();
var score = int.Parse(getScore.Result.Value.ToString());
//make new userscore to add to leader board
var userScore = new UserScore(name, score);
Debug.Log($"{name} : {score}");
return userScore;
});
tasks.Add(task);
}
var scores = await Task.WhenAll(tasks);
List<UserScore> leaderBoard = new List<UserScore>(scores);
This is mostly database structure issue.
First, you need leaderboard table.
leaderboard: {
<user_id>: <score>
}
Second, you need users table.
users: {
<user_id>: {
name: <user_name>,
...,
score: <user_score>,
friendlist: {
multiple friends user ids
}
}
}
And you have to update leaderboard's score and users' score at the same time.
If you want to avoid Callback hell.
You can also try like this. (This code is JAVA)
// Create a new ThreadPoolExecutor with 2 threads for each processor on the
// device and a 60 second keep-alive time.
int numCores = Runtime.getRuntime().availableProcessors();
ExecutorService executor = new ThreadPoolExecutor(
numCores * 2,
numCores * 2,
60L,
TimeUnit.SECONDS,
new LinkedBlockingQueue<Runnable>()
);
Tasks.call(executor, (Callable<Void>) () -> {
Task<Token> getStableTokenTask = NotificationUtil.getStableToken(token);
Token stableToken;
stableToken = Tasks.await(getStableTokenTask);
if (stableToken != null) {
Task<Void> updateStableTokenTask = NotificationUtil.updateStableToken(stableToken.getId(), versionCode, versionName);
Tasks.await(updateStableTokenTask);
}
if (stableToken == null) {
Token newToken = new Token(token, versionCode, versionName);
Task<Void> insertStableTokenTask = NotificationUtil.insertStableToken(newToken);
Tasks.await(insertStableTokenTask);
}
return null;
}).continueWith((Continuation<Void, Void>) task -> {
if (!task.isSuccessful()) {
// You will catch every exceptions from here.
Log.w(TAG, task.getException());
return null;
}
return null;
});

Why did I have to use Task<T> to acheive Synchronous calls?

Im working on a website that integrates with Dynamics 365 with the Dynamics SDK. We have seen errors in the logs such as “Cannot access a disposed object”. Upon further investigation we found out that the SDK methods are not thread safe so needed to refactor the code to take this into account.
We had a method such as follows that would create or update a Contact entity depending on whether it already exists:
public Guid? SetProfile(IProfile profile)
{
using (var xrm = new XrmServiceContext(_organizationService))
{
//check whether account already exists
var crmProfile = GetContact(xrm, profile.UserId);
if (crmProfile == null)
{
//create new account if required
{
crmProfile = new Contact
{
EMailAddress1 = profile.Username,
//lots of properties hidden to make for easier code example
};
}
xrm.AddObject(crmProfile);
}
else
{
//update existing account
crmProfile.new_Title = profile.Title.HasValue ? new OptionSetValue(profile.Title.Value) : null;
//lots of properties hidden to make for easier code example
xrm.UpdateObject(crmProfile);
}
var response = xrm.SaveChanges();
return crmProfile.Id;
}
}
When this method was executed concurrently by 2 or more users the error "Cannot access a disposed object" would be thrown, referring to the XrmServiceContext object.
I therefore knew that I needed to make this method thread-safe, but also it needs to be Synchronous as our UI depends on having the return value of the method. I played around with different threading methods:
Task.Factory.StartNew(() => delegate
new Thread()
However, with both of these methods I wasn't able to get the method to execute synchronously, so I ended up with:
public Guid? SetProfile(IProfile profile)
{
var task = new Task<Guid?>(() =>
{
using (var xrm = new XrmServiceContext(_organizationService))
{
//check whether account already exists
var crmProfile = GetContact(xrm, profile.UserId);
if (crmProfile == null)
{
//create new account if required
{
crmProfile = new Contact
{
EMailAddress1 = profile.Username,
//lots of properties hidden to make for easier code example
};
}
xrm.AddObject(crmProfile);
}
else
{
//update existing account
crmProfile.new_Title = profile.Title.HasValue ? new OptionSetValue(profile.Title.Value) : null;
//lots of properties hidden to make for easier code example
xrm.UpdateObject(crmProfile);
}
var response = xrm.SaveChanges();
return crmProfile.Id;
}
});
task.RunSynchronously();
return task.Result;
}
Everything I seemed to read online suggested I should use the StartNew method, however this is geared towards Asynchronous calls with I could not allow, and it also seemed that it doesnt guarantee a new thread - from what I've read I understand it is clever enough to know when it needs to create a new thread - however in my instance I have to be certain a new thread is used for the call to Dynamics.
Questions:
Anything wrong with the approach I've taken for a Web application?
If I can't use Asynchronous calls, is there any advantage whatsoever to using the StartNew method?
Many thanks for your time in advance
Kind regards
dotdev

Creating a tree of required items

As I am developing my small game, I have made a lot of progress yet frustrated about a lot of things. The latest thing was creating a list of required items and for you to understand that I will provide you with both Explanation as well as Code which I created but obviously doesn't work...
I - Explanation
In order for the player to build a building he must have done some required researches with each research requires more researches for it to be researched... It is like a tree of researches that the player will go through them by exploring the game and doing some tasks...
So to imagine it more accurately you can look at my small code here
II - Code
//Available Main Elements
var carbon = new Element {Name = "Carbon"};
var hydrogen = new Element {Name = "Hydrogen"};
var oxygen = new Element {Name = "Oxygen"};
var nitrogen = new Element {Name = "Nitrogen"};
//Example Research
var steam = new Research(name : "Steam", requiredElements: null, requiredResearches: /*Fire*/ & /*Water*/ & /*Iron*/);
So from the last snippet of code [which is just to explain] the player want to research the Steam that for instance needs 3 more researches in order to be researched... one of which the Iron also needs 1 more research to be researched and so on [maybe less maybe more or maybe no requirements at all]...
Concluding that the Question is : How could I create such nesting so that when a player tries to do a research the system quickly looks at the researches he have done and the research he wants to do [including it's nested ones] and If the player did not meet the requirements it just returns a tree with what things he want's to achieve ?
After all, I just want to thank you in advance and I am waiting for your very valuable support...
I would remove the requirement logic out of the Research objects themselves. For simplicity say it was this:
public class Research
{
public string Name { get; set; }
}
Then, I would keep a list of the requirements in a Dictonary where each Research contains a bucket of other Researches:
Dictionary<Research, List<Research>> requiredResearches =
new Dictionary<Research, List<Research>>();
// the list of Researches the player has completed
List<Research> playersResearched = new List<Research>;
For example, "Steam" would contain "Fire", "Water" and "Iron". And maybe "Iron" contains "Tin".
Next, given a Research, we could look at all of it's requierements including requirements of requirements:
// e.g. research is "Steam" and returns "Fire", "Water", "Iron", "Tin"
var chainOfRequirements = GetReq(requiredResearches, research);
That calls a recursive function like this:
public IList<Research> GetReq(Dictionary<Research, List<Research>> reqs,
Research target)
{
var chain = new List<Research>();
if(reqs.ContainsKey(target))
{
foreach(var item in reqs[target])
{
chain.Add(item);
chain.AddRange(GetReq(reqs, item));
}
}
return chain;
}
What you are returned is a flat list of requirements (including requirements of requirements). At that point, a little query against the players list of Researches can return to you which ones that are missing:
var missing = chainOfRequirements.Where (c =>
playerResearches.Where (r => r == c).Any () == false).Distinct();
TO ENABLE COMPARING DICTIONARY USING "NAME"
public sealed class NameEqualityComparer : IEqualityComparer<Research>
{
public bool Equals(Research x, Research y)
{
if (ReferenceEquals(x, y)) return true;
if (ReferenceEquals(x, null)) return false;
if (ReferenceEquals(y, null)) return false;
if (x.GetType() != y.GetType()) return false;
return string.Equals(x.Name, y.Name);
}
public int GetHashCode(Research obj)
{
return (obj.Name != null ? obj.Name.GetHashCode() : 0);
}
}
Here is proof of concept.
I don't know C# well enough to provide code, but you could make each Research have an Array of type Research, that would hold the Research that must be done to be able to make the actual Research, then you only need to iterate the Array checking if all of them are completed.
You don't even need to check the requirements of the requirements, as if they are completed, they already have all requirements completed.
If the Research don't have any requirements you just let the Array empty.
I suggest that you make your own requirementsTree class, which would need to be a linked list with at least the following members:
a requirementsTree[] prerequisiteResearch property, containing all of the research required for that particular item.
a bool isResearched property, indicating if the player has in fact researched the item.
a method, say, bool isEligibleToResearch(), which loops through all of the items in prerequisiteResearch to check if the player has already researched them.
With this I think you'd have a well-structured and extensible requirements class.
I can't think of any specific things that would prevent this from happening, but I can see why you'd be hesitant to just jump into coding it without having thought through the cases. To try to give some pointers, here are some functions I would imagine you having by the end:
// Having a function to retrieve a unique variable by its name (backed by a Dictionary<string, Research>)
// is often handy for me, especially if you decide to script the requirements tree in a text file.
// If you have enough references passed around that you can do this without a static method, all
// the better.
Research.getResearchByName(string name)
// A recursive function. If this research has not been completed, it adds itself to the set, as well
// as any prerequisites (by calling this function on its prerequisites). The top-level, public
// version of this function would create the set, and then return it after performing this check. If
// the Set is empty, then the player can start the research.
Research.addUnresearched_Internal(Set<Research> unresearched)
I think the main issue with my approach here is that I only thought to use a Set, rather than a Tree;
but with some bit of variation, you might be able to do better than me.
One possibility would be to add bool completed to the Element and Research classes. You could then have a function that checks for completed == false for any of its sub-researches.
bool CanCompleteResearch(Research r)
{
for (Research sub in r.requiredResearches)
{
// No need to recursively check 'sub'; in order for 'sub' to be marked as
// completed, its sub-researches must've been already completed.
if (!sub.completed)
return false;
}
return true;
}
I'm sure there's some one-liner trick you can do with LINQ.
EDIT: this only works if a research tree is not being used in more than one place at once (i.e. if you had the research objects shared across multiple player instances).
EDIT 2: In order to figure out all the missing research, you can use a simple recursive function:
void ListMissingResearch(Research r)
{
for (Research sub in r.requiredResearches)
{
if (!sub.completed)
{
// print message saying "Research <r> requires research <sub>"
ListMissingResearch(sub); // list any missing sub-sub-research
}
}
}
It sounds like you want to do something like this:
public class Research
{
private Collection<Research> prerequisites
public Collection<Research> Prerequisites
{
get { return this.prerequisistes; }
}
public bool IsComplete { get; set; }
public IEnumerable<Research> RequiredResearch()
{
if (this.IsComplete)
{
return new Research[0];
}
else
{
return new[] { this }.Concat(
this.Prerequisites.SelectMany(r => r.RequiredResearch()));
}
}
}
If you're making a multi-player, it might have to be something like this:
public class Player
{
private Collection<Research> completedResearch
public Collection<Research> CompletedResearch
{
get { return this.completedResearch; }
}
}
public class Research
{
private Collection<Research> prerequisites
public Collection<Research> Prerequisites
{
get { return this.prerequisistes; }
}
public IEnumerable<Research> RequiredResearch(Player player)
{
if (player.CompletedResearch.Contains(this))
{
return new Research[0];
}
else
{
return new[] { this }.Concat(
this.Prerequisites.SelectMany(r => r.RequiredResearch(player)));
}
}
}
Both these methods will return the current research if it has not been completed (instead of just it's prerequisites). You could simply do a .Skip(1) on the result to ignore the current one, though.
You could use a function like this in your research class that just takes in an enumerable list of Researchs you use to check that the player has the prerequisits. This assumes there is a local list or some other enumerable of Researches called Prereqs in the Research class.
public bool CanHas(IEnumerable<Research> researches)
{
return Prereqs.All((pr) => researches.Contains(pr) && pr.CanHas(researches));
}
The function just recursivly checks each Prereq to see if it is in the list passed in, and then checks that prereqs prereqs. All is a linq extention method that just returns true if all of the elements in the enumerable meet the criteria. If there are no elements, it returns true (all zero of the elements meet the criteria) so this will terminate when you get down to a Research with no Prereqs.
Note: This does a depth first search and is not smart about the same research being a sub-research of more then one prerequisite.

Categories

Resources